blob: c1011503dae77a84b1544227f2638d5ee6b458d4 [file] [log] [blame]
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001/*
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
Yohei Yukawab21220e2014-11-01 21:04:30 +090019import android.annotation.NonNull;
20import android.annotation.Nullable;
Yohei Yukawae63b5fa2014-09-19 13:14:55 +090021import android.app.AppOpsManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090022import android.content.ContentResolver;
23import android.content.Context;
24import android.content.pm.ApplicationInfo;
Yohei Yukawa094c71f2015-06-20 00:41:31 -070025import android.content.pm.IPackageManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090026import android.content.pm.PackageManager;
27import android.content.res.Resources;
Yohei Yukawa094c71f2015-06-20 00:41:31 -070028import android.os.RemoteException;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090029import android.provider.Settings;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090030import android.text.TextUtils;
Seigo Nonaka2028dda2015-07-06 17:41:24 +090031import android.text.TextUtils.SimpleStringSplitter;
32import android.util.ArrayMap;
33import android.util.ArraySet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090034import android.util.Pair;
35import android.util.Slog;
36import android.view.inputmethod.InputMethodInfo;
37import android.view.inputmethod.InputMethodSubtype;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +090038import android.view.textservice.SpellCheckerInfo;
39import android.view.textservice.TextServicesManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090040
Yohei Yukawae72d1c82015-02-20 20:55:21 +090041import com.android.internal.annotations.VisibleForTesting;
42
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090043import java.util.ArrayList;
Yohei Yukawab21220e2014-11-01 21:04:30 +090044import java.util.Arrays;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090045import java.util.HashMap;
Yohei Yukawab21220e2014-11-01 21:04:30 +090046import java.util.LinkedHashSet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090047import java.util.List;
48import java.util.Locale;
49
50/**
51 * InputMethodManagerUtils contains some static methods that provides IME informations.
52 * This methods are supposed to be used in both the framework and the Settings application.
53 */
54public class InputMethodUtils {
55 public static final boolean DEBUG = false;
56 public static final int NOT_A_SUBTYPE_ID = -1;
Yohei Yukawa68c860b2014-09-13 22:03:37 +090057 public static final String SUBTYPE_MODE_ANY = null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090058 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
59 public static final String SUBTYPE_MODE_VOICE = "voice";
60 private static final String TAG = "InputMethodUtils";
61 private static final Locale ENGLISH_LOCALE = new Locale("en");
62 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
63 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
64 "EnabledWhenDefaultIsNotAsciiCapable";
65 private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
Seigo Nonakace2c7842015-08-17 08:47:36 -070066
67 // The string for enabled input method is saved as follows:
68 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
Seigo Nonaka2028dda2015-07-06 17:41:24 +090069 private static final char INPUT_METHOD_SEPARATOR = ':';
70 private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
Yohei Yukawadc489242014-09-14 12:01:59 +090071 /**
72 * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
73 * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
74 * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
75 * doesn't automatically match {@code Locale("en", "IN")}.
76 */
77 private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
78 Locale.ENGLISH, // "en"
79 Locale.US, // "en_US"
80 Locale.UK, // "en_GB"
81 };
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090082
83 private InputMethodUtils() {
84 // This utility class is not publicly instantiable.
85 }
86
Satoshi Kataoka0766eb02013-07-31 18:30:13 +090087 // ----------------------------------------------------------------------
88 // Utilities for debug
Satoshi Kataoka87c29142013-07-31 23:11:54 +090089 public static String getApiCallStack() {
90 String apiCallStack = "";
91 try {
92 throw new RuntimeException();
93 } catch (RuntimeException e) {
94 final StackTraceElement[] frames = e.getStackTrace();
95 for (int j = 1; j < frames.length; ++j) {
96 final String tempCallStack = frames[j].toString();
97 if (TextUtils.isEmpty(apiCallStack)) {
98 // Overwrite apiCallStack if it's empty
99 apiCallStack = tempCallStack;
100 } else if (tempCallStack.indexOf("Transact(") < 0) {
101 // Overwrite apiCallStack if it's not a binder call
102 apiCallStack = tempCallStack;
103 } else {
104 break;
105 }
106 }
107 }
108 return apiCallStack;
109 }
Satoshi Kataoka0766eb02013-07-31 18:30:13 +0900110 // ----------------------------------------------------------------------
111
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900112 public static boolean isSystemIme(InputMethodInfo inputMethod) {
113 return (inputMethod.getServiceInfo().applicationInfo.flags
114 & ApplicationInfo.FLAG_SYSTEM) != 0;
115 }
116
Yohei Yukawa9c83ff42015-03-12 15:31:25 +0900117 public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900118 final Context context, final boolean checkDefaultAttribute,
119 @Nullable final Locale requiredLocale, final boolean checkCountry,
120 final String requiredSubtypeMode) {
121 if (!isSystemIme(imi)) {
122 return false;
123 }
124 if (checkDefaultAttribute && !imi.isDefault(context)) {
125 return false;
126 }
127 if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
128 return false;
129 }
130 return true;
131 }
132
133 @Nullable
Yohei Yukawadc489242014-09-14 12:01:59 +0900134 public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis,
135 final Context context) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900136 // At first, find the fallback locale from the IMEs that are declared as "default" in the
137 // current locale. Note that IME developers can declare an IME as "default" only for
138 // some particular locales but "not default" for other locales.
Yohei Yukawadc489242014-09-14 12:01:59 +0900139 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
140 for (int i = 0; i < imis.size(); ++i) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900141 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
142 true /* checkDefaultAttribute */, fallbackLocale,
143 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900144 return fallbackLocale;
145 }
146 }
147 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900148 // If no fallback locale is found in the above condition, find fallback locales regardless
149 // of the "default" attribute as a last resort.
150 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
151 for (int i = 0; i < imis.size(); ++i) {
152 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
153 false /* checkDefaultAttribute */, fallbackLocale,
154 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
155 return fallbackLocale;
156 }
157 }
158 }
159 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
Yohei Yukawadc489242014-09-14 12:01:59 +0900160 return null;
161 }
162
Yohei Yukawab21220e2014-11-01 21:04:30 +0900163 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi,
164 final Context context, final boolean checkDefaultAttribute) {
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900165 if (!isSystemIme(imi)) {
166 return false;
167 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900168 if (checkDefaultAttribute && !imi.isDefault(context)) {
169 return false;
170 }
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900171 if (!imi.isAuxiliaryIme()) {
172 return false;
173 }
174 final int subtypeCount = imi.getSubtypeCount();
175 for (int i = 0; i < subtypeCount; ++i) {
176 final InputMethodSubtype s = imi.getSubtypeAt(i);
177 if (s.overridesImplicitlyEnabledSubtype()) {
178 return true;
179 }
180 }
181 return false;
182 }
183
Yohei Yukawadc489242014-09-14 12:01:59 +0900184 public static Locale getSystemLocaleFromContext(final Context context) {
185 try {
186 return context.getResources().getConfiguration().locale;
187 } catch (Resources.NotFoundException ex) {
188 return null;
189 }
190 }
191
Yohei Yukawab21220e2014-11-01 21:04:30 +0900192 private static final class InputMethodListBuilder {
193 // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
194 // order can have non-trivial effect in the call sites.
195 @NonNull
196 private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
Yohei Yukawadc489242014-09-14 12:01:59 +0900197
Yohei Yukawab21220e2014-11-01 21:04:30 +0900198 public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis,
199 final Context context, final boolean checkDefaultAttribute,
200 @Nullable final Locale locale, final boolean checkCountry,
201 final String requiredSubtypeMode) {
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900202 for (int i = 0; i < imis.size(); ++i) {
203 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900204 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
205 checkCountry, requiredSubtypeMode)) {
206 mInputMethodSet.add(imi);
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900207 }
208 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900209 return this;
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900210 }
211
Yohei Yukawab21220e2014-11-01 21:04:30 +0900212 // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
213 // documented more clearly.
214 public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis,
215 final Context context) {
216 // If one or more auxiliary input methods are available, OK to stop populating the list.
217 for (final InputMethodInfo imi : mInputMethodSet) {
218 if (imi.isAuxiliaryIme()) {
219 return this;
220 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900221 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900222 boolean added = false;
Yohei Yukawadc489242014-09-14 12:01:59 +0900223 for (int i = 0; i < imis.size(); ++i) {
224 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900225 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
226 true /* checkDefaultAttribute */)) {
227 mInputMethodSet.add(imi);
228 added = true;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900229 }
230 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900231 if (added) {
232 return this;
233 }
234 for (int i = 0; i < imis.size(); ++i) {
235 final InputMethodInfo imi = imis.get(i);
236 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
237 false /* checkDefaultAttribute */)) {
238 mInputMethodSet.add(imi);
239 }
240 }
241 return this;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900242 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900243
Yohei Yukawab21220e2014-11-01 21:04:30 +0900244 public boolean isEmpty() {
245 return mInputMethodSet.isEmpty();
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900246 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900247
248 @NonNull
249 public ArrayList<InputMethodInfo> build() {
250 return new ArrayList<>(mInputMethodSet);
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900251 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900252 }
253
Yohei Yukawab21220e2014-11-01 21:04:30 +0900254 private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale(
255 final ArrayList<InputMethodInfo> imis, final Context context,
256 @Nullable final Locale fallbackLocale) {
257 // Before the system becomes ready, we pick up at least one keyboard in the following order.
258 // The first user (device owner) falls into this category.
259 // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
260 // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
261 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
262 // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
263 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
264
265 final InputMethodListBuilder builder = new InputMethodListBuilder();
266 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
267 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
268 if (!builder.isEmpty()) {
269 return builder;
Yohei Yukawadc489242014-09-14 12:01:59 +0900270 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900271 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
272 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
273 if (!builder.isEmpty()) {
274 return builder;
275 }
276 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
277 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
278 if (!builder.isEmpty()) {
279 return builder;
280 }
281 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
282 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
283 if (!builder.isEmpty()) {
284 return builder;
285 }
286 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
287 + " fallbackLocale=" + fallbackLocale);
288 return builder;
289 }
290
291 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
292 final ArrayList<InputMethodInfo> imis, final Context context,
293 @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
294 // Once the system becomes ready, we pick up at least one keyboard in the following order.
295 // Secondary users fall into this category in general.
296 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
297 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
298 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
299 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
300 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
301 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
302 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
303
304 final InputMethodListBuilder builder = new InputMethodListBuilder();
305 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
306 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
307 if (!builder.isEmpty()) {
308 return builder;
309 }
310 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
311 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
312 if (!builder.isEmpty()) {
313 return builder;
314 }
315 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
316 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
317 if (!builder.isEmpty()) {
318 return builder;
319 }
320 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
321 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
322 if (!builder.isEmpty()) {
323 return builder;
324 }
325 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
326 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
327 if (!builder.isEmpty()) {
328 return builder;
329 }
330 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
331 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
332 if (!builder.isEmpty()) {
333 return builder;
334 }
335 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
336 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
337 return builder;
338 }
339
340 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
341 final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) {
342 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
343 if (!isSystemReady) {
344 // When the system is not ready, the system locale is not stable and reliable. Hence
345 // we will pick up IMEs that support software keyboard based on the fallback locale.
346 // Also pick up suitable IMEs regardless of the software keyboard support.
347 // (e.g. Voice IMEs)
348 return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale)
349 .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
350 true /* checkCountry */, SUBTYPE_MODE_ANY)
351 .build();
352 }
353
354 // When the system is ready, we will primarily rely on the system locale, but also keep
355 // relying on the fallback locale as a last resort.
356 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
357 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
358 // subtype)
359 final Locale systemLocale = getSystemLocaleFromContext(context);
360 return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
361 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
362 true /* checkCountry */, SUBTYPE_MODE_ANY)
363 .fillAuxiliaryImes(imis, context)
364 .build();
Yohei Yukawadc489242014-09-14 12:01:59 +0900365 }
366
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900367 public static Locale constructLocaleFromString(String localeStr) {
368 if (TextUtils.isEmpty(localeStr)) {
369 return null;
370 }
371 // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
372 String[] localeParams = localeStr.split("_", 3);
Yohei Yukawaed65bc02015-12-02 18:22:41 -0800373 if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
374 // Convert a locale whose language is "tl" to one whose language is "fil".
375 // For example, "tl_PH" will get converted to "fil_PH".
376 // Versions of Android earlier than Lollipop did not support three letter language
377 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
378 // On Lollipop and above, the current three letter version must be used.
379 localeParams[0] = "fil";
380 }
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900381 // The length of localeStr is guaranteed to always return a 1 <= value <= 3
382 // because localeStr is not empty.
383 if (localeParams.length == 1) {
384 return new Locale(localeParams[0]);
385 } else if (localeParams.length == 2) {
386 return new Locale(localeParams[0], localeParams[1]);
387 } else if (localeParams.length == 3) {
388 return new Locale(localeParams[0], localeParams[1], localeParams[2]);
389 }
390 return null;
391 }
392
Yohei Yukawadc489242014-09-14 12:01:59 +0900393 public static boolean containsSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900394 @Nullable final Locale locale, final boolean checkCountry, final String mode) {
395 if (locale == null) {
396 return false;
397 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900398 final int N = imi.getSubtypeCount();
399 for (int i = 0; i < N; ++i) {
400 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900401 if (checkCountry) {
Yohei Yukawa92280cd2015-06-02 16:50:14 -0700402 final Locale subtypeLocale = subtype.getLocaleObject();
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900403 if (subtypeLocale == null ||
404 !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
405 !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900406 continue;
407 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900408 } else {
409 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
410 subtype.getLocale()));
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900411 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900412 continue;
413 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900414 }
415 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
416 mode.equalsIgnoreCase(subtype.getMode())) {
417 return true;
418 }
419 }
420 return false;
421 }
422
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900423 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700424 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900425 final int subtypeCount = imi.getSubtypeCount();
426 for (int i = 0; i < subtypeCount; ++i) {
427 subtypes.add(imi.getSubtypeAt(i));
428 }
429 return subtypes;
430 }
431
432 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
433 InputMethodInfo imi, String mode) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700434 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900435 final int subtypeCount = imi.getSubtypeCount();
436 for (int i = 0; i < subtypeCount; ++i) {
437 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
438 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
439 subtypes.add(subtype);
440 }
441 }
442 return subtypes;
443 }
444
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900445 public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900446 if (enabledImes == null || enabledImes.isEmpty()) {
447 return null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900448 }
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900449 // We'd prefer to fall back on a system IME, since that is safer.
450 int i = enabledImes.size();
451 int firstFoundSystemIme = -1;
452 while (i > 0) {
453 i--;
454 final InputMethodInfo imi = enabledImes.get(i);
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900455 if (imi.isAuxiliaryIme()) {
456 continue;
457 }
458 if (InputMethodUtils.isSystemIme(imi)
459 && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
460 SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900461 return imi;
462 }
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900463 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900464 firstFoundSystemIme = i;
465 }
466 }
467 return enabledImes.get(Math.max(firstFoundSystemIme, 0));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900468 }
469
470 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
471 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
472 }
473
474 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
475 if (imi != null) {
476 final int subtypeCount = imi.getSubtypeCount();
477 for (int i = 0; i < subtypeCount; ++i) {
478 InputMethodSubtype ims = imi.getSubtypeAt(i);
479 if (subtypeHashCode == ims.hashCode()) {
480 return i;
481 }
482 }
483 }
484 return NOT_A_SUBTYPE_ID;
485 }
486
Yohei Yukawae72d1c82015-02-20 20:55:21 +0900487 @VisibleForTesting
488 public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900489 Resources res, InputMethodInfo imi) {
490 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
491 final String systemLocale = res.getConfiguration().locale.toString();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700492 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100493 final String systemLanguage = res.getConfiguration().locale.getLanguage();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700494 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900495 final int N = subtypes.size();
496 for (int i = 0; i < N; ++i) {
497 // scan overriding implicitly enabled subtypes.
498 InputMethodSubtype subtype = subtypes.get(i);
499 if (subtype.overridesImplicitlyEnabledSubtype()) {
500 final String mode = subtype.getMode();
501 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
502 applicableModeAndSubtypesMap.put(mode, subtype);
503 }
504 }
505 }
506 if (applicableModeAndSubtypesMap.size() > 0) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700507 return new ArrayList<>(applicableModeAndSubtypesMap.values());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900508 }
509 for (int i = 0; i < N; ++i) {
510 final InputMethodSubtype subtype = subtypes.get(i);
511 final String locale = subtype.getLocale();
512 final String mode = subtype.getMode();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100513 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900514 // When system locale starts with subtype's locale, that subtype will be applicable
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100515 // for system locale. We need to make sure the languages are the same, to prevent
516 // locales like "fil" (Filipino) being matched by "fi" (Finnish).
517 //
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900518 // For instance, it's clearly applicable for cases like system locale = en_US and
519 // subtype = en, but it is not necessarily considered applicable for cases like system
520 // locale = en and subtype = en_US.
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100521 //
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900522 // We just call systemLocale.startsWith(locale) in this function because there is no
523 // need to find applicable subtypes aggressively unlike
524 // findLastResortApplicableSubtypeLocked.
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100525 //
526 // TODO: This check is broken. It won't take scripts into account and doesn't
527 // account for the mandatory conversions performed by Locale#toString.
528 if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900529 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
530 // If more applicable subtypes are contained, skip.
531 if (applicableSubtype != null) {
532 if (systemLocale.equals(applicableSubtype.getLocale())) continue;
533 if (!systemLocale.equals(locale)) continue;
534 }
535 applicableModeAndSubtypesMap.put(mode, subtype);
536 }
537 }
538 final InputMethodSubtype keyboardSubtype
539 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700540 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900541 applicableModeAndSubtypesMap.values());
542 if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
543 for (int i = 0; i < N; ++i) {
544 final InputMethodSubtype subtype = subtypes.get(i);
545 final String mode = subtype.getMode();
546 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
547 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
548 applicableSubtypes.add(subtype);
549 }
550 }
551 }
552 if (keyboardSubtype == null) {
553 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
554 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
555 if (lastResortKeyboardSubtype != null) {
556 applicableSubtypes.add(lastResortKeyboardSubtype);
557 }
558 }
559 return applicableSubtypes;
560 }
561
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900562 /**
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100563 * Returns the language component of a given locale string.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900564 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100565 */
Tadashi G. Takaoka77cbcb62014-07-12 16:08:20 +0900566 public static String getLanguageFromLocaleString(String locale) {
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100567 final int idx = locale.indexOf('_');
568 if (idx < 0) {
569 return locale;
570 } else {
571 return locale.substring(0, idx);
572 }
573 }
574
575 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900576 * If there are no selected subtypes, tries finding the most applicable one according to the
577 * given locale.
578 * @param subtypes this function will search the most applicable subtype in subtypes
579 * @param mode subtypes will be filtered by mode
580 * @param locale subtypes will be filtered by locale
581 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
582 * it will return the first subtype matched with mode
583 * @return the most applicable subtypeId
584 */
585 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
586 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
587 boolean canIgnoreLocaleAsLastResort) {
588 if (subtypes == null || subtypes.size() == 0) {
589 return null;
590 }
591 if (TextUtils.isEmpty(locale)) {
592 locale = res.getConfiguration().locale.toString();
593 }
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100594 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900595 boolean partialMatchFound = false;
596 InputMethodSubtype applicableSubtype = null;
597 InputMethodSubtype firstMatchedModeSubtype = null;
598 final int N = subtypes.size();
599 for (int i = 0; i < N; ++i) {
600 InputMethodSubtype subtype = subtypes.get(i);
601 final String subtypeLocale = subtype.getLocale();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100602 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900603 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
604 // and all subtypes with all modes can be candidates.
605 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
606 if (firstMatchedModeSubtype == null) {
607 firstMatchedModeSubtype = subtype;
608 }
609 if (locale.equals(subtypeLocale)) {
610 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
611 applicableSubtype = subtype;
612 break;
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100613 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900614 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
615 applicableSubtype = subtype;
616 partialMatchFound = true;
617 }
618 }
619 }
620
621 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
622 return firstMatchedModeSubtype;
623 }
624
625 // The first subtype applicable to the system locale will be defined as the most applicable
626 // subtype.
627 if (DEBUG) {
628 if (applicableSubtype != null) {
629 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
630 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
631 }
632 }
633 return applicableSubtype;
634 }
635
636 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
637 if (subtype == null) return true;
638 return !subtype.isAuxiliary();
639 }
640
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900641 public static void setNonSelectedSystemImesDisabledUntilUsed(
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700642 IPackageManager packageManager, List<InputMethodInfo> enabledImis,
643 int userId, String callingPackage) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900644 if (DEBUG) {
645 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
646 }
647 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
648 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
649 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
650 return;
651 }
652 // Only the current spell checker should be treated as an enabled one.
653 final SpellCheckerInfo currentSpellChecker =
654 TextServicesManager.getInstance().getCurrentSpellChecker();
655 for (final String packageName : systemImesDisabledUntilUsed) {
656 if (DEBUG) {
657 Slog.d(TAG, "check " + packageName);
658 }
659 boolean enabledIme = false;
660 for (int j = 0; j < enabledImis.size(); ++j) {
661 final InputMethodInfo imi = enabledImis.get(j);
662 if (packageName.equals(imi.getPackageName())) {
663 enabledIme = true;
664 break;
665 }
666 }
667 if (enabledIme) {
668 // enabled ime. skip
669 continue;
670 }
671 if (currentSpellChecker != null
672 && packageName.equals(currentSpellChecker.getPackageName())) {
673 // enabled spell checker. skip
674 if (DEBUG) {
675 Slog.d(TAG, packageName + " is the current spell checker. skip");
676 }
677 continue;
678 }
679 ApplicationInfo ai = null;
680 try {
681 ai = packageManager.getApplicationInfo(packageName,
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700682 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
683 } catch (RemoteException e) {
684 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
685 + " userId=" + userId, e);
686 continue;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900687 }
688 if (ai == null) {
689 // No app found for packageName
690 continue;
691 }
692 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
693 if (!isSystemPackage) {
694 continue;
695 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700696 setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900697 }
698 }
699
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700700 private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
701 int userId, String callingPackage) {
702 final int state;
703 try {
704 state = packageManager.getApplicationEnabledSetting(packageName, userId);
705 } catch (RemoteException e) {
706 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
707 + " userId=" + userId, e);
708 return;
709 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900710 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
711 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
712 if (DEBUG) {
713 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
714 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700715 try {
716 packageManager.setApplicationEnabledSetting(packageName,
717 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
718 0 /* newState */, userId, callingPackage);
719 } catch (RemoteException e) {
720 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
721 + " userId=" + userId + " callingPackage=" + callingPackage, e);
722 return;
723 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900724 } else {
725 if (DEBUG) {
726 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
727 }
728 }
729 }
730
Satoshi Kataokab2827262013-07-04 19:43:14 +0900731 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
732 InputMethodSubtype subtype) {
733 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
734 return subtype != null
735 ? TextUtils.concat(subtype.getDisplayName(context,
736 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
737 (TextUtils.isEmpty(imiLabel) ?
738 "" : " - " + imiLabel))
739 : imiLabel;
740 }
741
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900742 /**
Yohei Yukawae63b5fa2014-09-19 13:14:55 +0900743 * Returns true if a package name belongs to a UID.
744 *
745 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
746 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
747 * @param uid the UID to be validated.
748 * @param packageName the package name.
749 * @return {@code true} if the package name belongs to the UID.
750 */
751 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
752 final int uid, final String packageName) {
753 try {
754 appOpsManager.checkPackage(uid, packageName);
755 return true;
756 } catch (SecurityException e) {
757 return false;
758 }
759 }
760
761 /**
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900762 * Parses the setting stored input methods and subtypes string value.
763 *
764 * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
765 * @return Map from input method ID to set of input method subtypes IDs.
766 */
767 @VisibleForTesting
768 public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
769 @Nullable final String inputMethodsAndSubtypesString) {
770
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800771 final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900772 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
773 return imeMap;
774 }
775
776 final SimpleStringSplitter typeSplitter =
777 new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
778 final SimpleStringSplitter subtypeSplitter =
779 new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
780
781 List<Pair<String, ArrayList<String>>> allImeSettings =
782 InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
783 typeSplitter,
784 subtypeSplitter);
785 for (Pair<String, ArrayList<String>> ime : allImeSettings) {
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800786 ArraySet<String> subtypes = new ArraySet<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900787 if (ime.second != null) {
788 subtypes.addAll(ime.second);
789 }
790 imeMap.put(ime.first, subtypes);
791 }
792 return imeMap;
793 }
794
Seigo Nonaka2a099bc2015-08-14 19:29:45 -0700795 @NonNull
796 public static String buildInputMethodsAndSubtypesString(
797 @NonNull final ArrayMap<String, ArraySet<String>> map) {
798 // we want to use the canonical InputMethodSettings implementation,
799 // so we convert data structures first.
800 List<Pair<String, ArrayList<String>>> imeMap = new ArrayList<>(4);
801 for (ArrayMap.Entry<String, ArraySet<String>> entry : map.entrySet()) {
802 final String imeName = entry.getKey();
803 final ArraySet<String> subtypeSet = entry.getValue();
804 final ArrayList<String> subtypes = new ArrayList<>(2);
805 if (subtypeSet != null) {
806 subtypes.addAll(subtypeSet);
807 }
808 imeMap.add(new Pair<>(imeName, subtypes));
809 }
810 return InputMethodSettings.buildInputMethodsSettingString(imeMap);
811 }
812
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900813 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900814 * Utility class for putting and getting settings for InputMethod
815 * TODO: Move all putters and getters of settings to this class.
816 */
817 public static class InputMethodSettings {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900818 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700819 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900820
821 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700822 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900823
824 private final Resources mRes;
825 private final ContentResolver mResolver;
826 private final HashMap<String, InputMethodInfo> mMethodMap;
827 private final ArrayList<InputMethodInfo> mMethodList;
828
829 private String mEnabledInputMethodsStrCache;
830 private int mCurrentUserId;
Kenny Guy2a764942014-04-02 13:29:20 +0100831 private int[] mCurrentProfileIds = new int[0];
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900832
833 private static void buildEnabledInputMethodsSettingString(
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700834 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
835 builder.append(ime.first);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900836 // Inputmethod and subtypes are saved in the settings as follows:
837 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700838 for (String subtypeId: ime.second) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700839 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900840 }
841 }
842
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700843 public static String buildInputMethodsSettingString(
844 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
845 final StringBuilder b = new StringBuilder();
846 boolean needsSeparator = false;
847 for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
848 if (needsSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700849 b.append(INPUT_METHOD_SEPARATOR);
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700850 }
851 buildEnabledInputMethodsSettingString(b, ime);
852 needsSeparator = true;
853 }
854 return b.toString();
855 }
856
857 public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
858 String enabledInputMethodsStr,
859 TextUtils.SimpleStringSplitter inputMethodSplitter,
860 TextUtils.SimpleStringSplitter subtypeSplitter) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700861 ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700862 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
863 return imsList;
864 }
865 inputMethodSplitter.setString(enabledInputMethodsStr);
866 while (inputMethodSplitter.hasNext()) {
867 String nextImsStr = inputMethodSplitter.next();
868 subtypeSplitter.setString(nextImsStr);
869 if (subtypeSplitter.hasNext()) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700870 ArrayList<String> subtypeHashes = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700871 // The first element is ime id.
872 String imeId = subtypeSplitter.next();
873 while (subtypeSplitter.hasNext()) {
874 subtypeHashes.add(subtypeSplitter.next());
875 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700876 imsList.add(new Pair<>(imeId, subtypeHashes));
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700877 }
878 }
879 return imsList;
880 }
881
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900882 public InputMethodSettings(
883 Resources res, ContentResolver resolver,
884 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
885 int userId) {
886 setCurrentUserId(userId);
887 mRes = res;
888 mResolver = resolver;
889 mMethodMap = methodMap;
890 mMethodList = methodList;
891 }
892
893 public void setCurrentUserId(int userId) {
894 if (DEBUG) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900895 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900896 }
897 // IMMS settings are kept per user, so keep track of current user
898 mCurrentUserId = userId;
899 }
900
Yohei Yukawa87523672016-02-12 19:37:08 -0800901 private void putString(final String key, final String str) {
902 Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId);
903 }
904
905 private String getString(final String key) {
906 return Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
907 }
908
909 private void putInt(final String key, final int value) {
910 Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId);
911 }
912
913 private int getInt(final String key, final int defaultValue) {
914 return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
915 }
916
917 private void putBoolean(final String key, final boolean value) {
918 Settings.Secure.putIntForUser(mResolver, key, value ? 1 : 0, mCurrentUserId);
919 }
920
921 private boolean getBoolean(final String key, final boolean defaultValue) {
922 return Settings.Secure.getIntForUser(mResolver, key, defaultValue ? 1 : 0,
923 mCurrentUserId) == 1;
924 }
925
Kenny Guy2a764942014-04-02 13:29:20 +0100926 public void setCurrentProfileIds(int[] currentProfileIds) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800927 synchronized (this) {
Kenny Guy2a764942014-04-02 13:29:20 +0100928 mCurrentProfileIds = currentProfileIds;
Amith Yamasani734983f2014-03-04 16:48:05 -0800929 }
930 }
931
Kenny Guy2a764942014-04-02 13:29:20 +0100932 public boolean isCurrentProfile(int userId) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800933 synchronized (this) {
Kenny Guyf4824a02014-04-02 19:17:41 +0100934 if (userId == mCurrentUserId) return true;
Kenny Guy2a764942014-04-02 13:29:20 +0100935 for (int i = 0; i < mCurrentProfileIds.length; i++) {
936 if (userId == mCurrentProfileIds[i]) return true;
Amith Yamasani734983f2014-03-04 16:48:05 -0800937 }
938 return false;
939 }
940 }
941
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900942 public List<InputMethodInfo> getEnabledInputMethodListLocked() {
943 return createEnabledInputMethodListLocked(
944 getEnabledInputMethodsAndSubtypeListLocked());
945 }
946
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900947 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
948 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
949 List<InputMethodSubtype> enabledSubtypes =
950 getEnabledInputMethodSubtypeListLocked(imi);
951 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
952 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
953 context.getResources(), imi);
954 }
955 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
956 }
957
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +0900958 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900959 InputMethodInfo imi) {
960 List<Pair<String, ArrayList<String>>> imsList =
961 getEnabledInputMethodsAndSubtypeListLocked();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700962 ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900963 if (imi != null) {
964 for (Pair<String, ArrayList<String>> imsPair : imsList) {
965 InputMethodInfo info = mMethodMap.get(imsPair.first);
966 if (info != null && info.getId().equals(imi.getId())) {
967 final int subtypeCount = info.getSubtypeCount();
968 for (int i = 0; i < subtypeCount; ++i) {
969 InputMethodSubtype ims = info.getSubtypeAt(i);
970 for (String s: imsPair.second) {
971 if (String.valueOf(ims.hashCode()).equals(s)) {
972 enabledSubtypes.add(ims);
973 }
974 }
975 }
976 break;
977 }
978 }
979 }
980 return enabledSubtypes;
981 }
982
983 // At the initial boot, the settings for input methods are not set,
984 // so we need to enable IME in that case.
985 public void enableAllIMEsIfThereIsNoEnabledIME() {
986 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
987 StringBuilder sb = new StringBuilder();
988 final int N = mMethodList.size();
989 for (int i = 0; i < N; i++) {
990 InputMethodInfo imi = mMethodList.get(i);
991 Slog.i(TAG, "Adding: " + imi.getId());
992 if (i > 0) sb.append(':');
993 sb.append(imi.getId());
994 }
995 putEnabledInputMethodsStr(sb.toString());
996 }
997 }
998
999 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
Christopher Tate7b9a28c2015-03-18 13:06:16 -07001000 return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
1001 mInputMethodSplitter,
1002 mSubtypeSplitter);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001003 }
1004
1005 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
1006 if (reloadInputMethodStr) {
1007 getEnabledInputMethodsStr();
1008 }
1009 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
1010 // Add in the newly enabled input method.
1011 putEnabledInputMethodsStr(id);
1012 } else {
1013 putEnabledInputMethodsStr(
Seigo Nonakace2c7842015-08-17 08:47:36 -07001014 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001015 }
1016 }
1017
1018 /**
1019 * Build and put a string of EnabledInputMethods with removing specified Id.
1020 * @return the specified id was removed or not.
1021 */
1022 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
1023 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
1024 boolean isRemoved = false;
1025 boolean needsAppendSeparator = false;
1026 for (Pair<String, ArrayList<String>> ims: imsList) {
1027 String curId = ims.first;
1028 if (curId.equals(id)) {
1029 // We are disabling this input method, and it is
1030 // currently enabled. Skip it to remove from the
1031 // new list.
1032 isRemoved = true;
1033 } else {
1034 if (needsAppendSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001035 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001036 } else {
1037 needsAppendSeparator = true;
1038 }
1039 buildEnabledInputMethodsSettingString(builder, ims);
1040 }
1041 }
1042 if (isRemoved) {
1043 // Update the setting with the new list of input methods.
1044 putEnabledInputMethodsStr(builder.toString());
1045 }
1046 return isRemoved;
1047 }
1048
1049 private List<InputMethodInfo> createEnabledInputMethodListLocked(
1050 List<Pair<String, ArrayList<String>>> imsList) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001051 final ArrayList<InputMethodInfo> res = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001052 for (Pair<String, ArrayList<String>> ims: imsList) {
1053 InputMethodInfo info = mMethodMap.get(ims.first);
1054 if (info != null) {
1055 res.add(info);
1056 }
1057 }
1058 return res;
1059 }
1060
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001061 private void putEnabledInputMethodsStr(String str) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001062 if (DEBUG) {
1063 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1064 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001065 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
1066 mEnabledInputMethodsStrCache = str;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001067 }
1068
Dianne Hackbornfd7aded2013-01-22 17:10:23 -08001069 public String getEnabledInputMethodsStr() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001070 mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001071 if (DEBUG) {
1072 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1073 + ", " + mCurrentUserId);
1074 }
1075 return mEnabledInputMethodsStrCache;
1076 }
1077
1078 private void saveSubtypeHistory(
1079 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1080 StringBuilder builder = new StringBuilder();
1081 boolean isImeAdded = false;
1082 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001083 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001084 newSubtypeId);
1085 isImeAdded = true;
1086 }
1087 for (Pair<String, String> ime: savedImes) {
1088 String imeId = ime.first;
1089 String subtypeId = ime.second;
1090 if (TextUtils.isEmpty(subtypeId)) {
1091 subtypeId = NOT_A_SUBTYPE_ID_STR;
1092 }
1093 if (isImeAdded) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001094 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001095 } else {
1096 isImeAdded = true;
1097 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001098 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001099 subtypeId);
1100 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001101 // Remove the last INPUT_METHOD_SEPARATOR
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001102 putSubtypeHistoryStr(builder.toString());
1103 }
1104
1105 private void addSubtypeToHistory(String imeId, String subtypeId) {
1106 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1107 for (Pair<String, String> ime: subtypeHistory) {
1108 if (ime.first.equals(imeId)) {
1109 if (DEBUG) {
1110 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1111 + ime.second);
1112 }
1113 // We should break here
1114 subtypeHistory.remove(ime);
1115 break;
1116 }
1117 }
1118 if (DEBUG) {
1119 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1120 }
1121 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1122 }
1123
1124 private void putSubtypeHistoryStr(String str) {
1125 if (DEBUG) {
1126 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1127 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001128 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001129 }
1130
1131 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1132 // Gets the first one from the history
1133 return getLastSubtypeForInputMethodLockedInternal(null);
1134 }
1135
1136 public String getLastSubtypeForInputMethodLocked(String imeId) {
1137 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1138 if (ime != null) {
1139 return ime.second;
1140 } else {
1141 return null;
1142 }
1143 }
1144
1145 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1146 List<Pair<String, ArrayList<String>>> enabledImes =
1147 getEnabledInputMethodsAndSubtypeListLocked();
1148 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1149 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1150 final String imeInTheHistory = imeAndSubtype.first;
1151 // If imeId is empty, returns the first IME and subtype in the history
1152 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1153 final String subtypeInTheHistory = imeAndSubtype.second;
1154 final String subtypeHashCode =
1155 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1156 enabledImes, imeInTheHistory, subtypeInTheHistory);
1157 if (!TextUtils.isEmpty(subtypeHashCode)) {
1158 if (DEBUG) {
1159 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1160 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001161 return new Pair<>(imeInTheHistory, subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001162 }
1163 }
1164 }
1165 if (DEBUG) {
1166 Slog.d(TAG, "No enabled IME found in the history");
1167 }
1168 return null;
1169 }
1170
1171 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1172 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1173 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1174 if (enabledIme.first.equals(imeId)) {
1175 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1176 final InputMethodInfo imi = mMethodMap.get(imeId);
1177 if (explicitlyEnabledSubtypes.size() == 0) {
1178 // If there are no explicitly enabled subtypes, applicable subtypes are
1179 // enabled implicitly.
1180 // If IME is enabled and no subtypes are enabled, applicable subtypes
1181 // are enabled implicitly, so needs to treat them to be enabled.
1182 if (imi != null && imi.getSubtypeCount() > 0) {
1183 List<InputMethodSubtype> implicitlySelectedSubtypes =
1184 getImplicitlyApplicableSubtypesLocked(mRes, imi);
1185 if (implicitlySelectedSubtypes != null) {
1186 final int N = implicitlySelectedSubtypes.size();
1187 for (int i = 0; i < N; ++i) {
1188 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1189 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1190 return subtypeHashCode;
1191 }
1192 }
1193 }
1194 }
1195 } else {
1196 for (String s: explicitlyEnabledSubtypes) {
1197 if (s.equals(subtypeHashCode)) {
1198 // If both imeId and subtypeId are enabled, return subtypeId.
1199 try {
1200 final int hashCode = Integer.valueOf(subtypeHashCode);
1201 // Check whether the subtype id is valid or not
1202 if (isValidSubtypeId(imi, hashCode)) {
1203 return s;
1204 } else {
1205 return NOT_A_SUBTYPE_ID_STR;
1206 }
1207 } catch (NumberFormatException e) {
1208 return NOT_A_SUBTYPE_ID_STR;
1209 }
1210 }
1211 }
1212 }
1213 // If imeId was enabled but subtypeId was disabled.
1214 return NOT_A_SUBTYPE_ID_STR;
1215 }
1216 }
1217 // If both imeId and subtypeId are disabled, return null
1218 return null;
1219 }
1220
1221 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001222 ArrayList<Pair<String, String>> imsList = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001223 final String subtypeHistoryStr = getSubtypeHistoryStr();
1224 if (TextUtils.isEmpty(subtypeHistoryStr)) {
1225 return imsList;
1226 }
1227 mInputMethodSplitter.setString(subtypeHistoryStr);
1228 while (mInputMethodSplitter.hasNext()) {
1229 String nextImsStr = mInputMethodSplitter.next();
1230 mSubtypeSplitter.setString(nextImsStr);
1231 if (mSubtypeSplitter.hasNext()) {
1232 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1233 // The first element is ime id.
1234 String imeId = mSubtypeSplitter.next();
1235 while (mSubtypeSplitter.hasNext()) {
1236 subtypeId = mSubtypeSplitter.next();
1237 break;
1238 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001239 imsList.add(new Pair<>(imeId, subtypeId));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001240 }
1241 }
1242 return imsList;
1243 }
1244
1245 private String getSubtypeHistoryStr() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001246 final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001247 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001248 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001249 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001250 return history;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001251 }
1252
1253 public void putSelectedInputMethod(String imeId) {
1254 if (DEBUG) {
1255 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1256 + mCurrentUserId);
1257 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001258 putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001259 }
1260
1261 public void putSelectedSubtype(int subtypeId) {
1262 if (DEBUG) {
1263 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1264 + mCurrentUserId);
1265 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001266 putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001267 }
1268
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001269 public String getSelectedInputMethod() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001270 final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001271 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001272 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001273 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001274 return imi;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001275 }
1276
1277 public boolean isSubtypeSelected() {
1278 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1279 }
1280
1281 private int getSelectedInputMethodSubtypeHashCode() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001282 return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001283 }
1284
Michael Wright7b5a96b2014-08-09 19:28:42 -07001285 public boolean isShowImeWithHardKeyboardEnabled() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001286 return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001287 }
1288
1289 public void setShowImeWithHardKeyboard(boolean show) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001290 putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001291 }
1292
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001293 public int getCurrentUserId() {
1294 return mCurrentUserId;
1295 }
1296
1297 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
1298 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1299 if (imi == null) {
1300 return NOT_A_SUBTYPE_ID;
1301 }
1302 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1303 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1304 }
1305
1306 public void saveCurrentInputMethodAndSubtypeToHistory(
1307 String curMethodId, InputMethodSubtype currentSubtype) {
1308 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1309 if (currentSubtype != null) {
1310 subtypeId = String.valueOf(currentSubtype.hashCode());
1311 }
1312 if (canAddToLastInputMethod(currentSubtype)) {
1313 addSubtypeToHistory(curMethodId, subtypeId);
1314 }
1315 }
Satoshi Kataokad787f692013-10-26 04:44:21 +09001316
1317 public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1318 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1319 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001320 new HashMap<>();
Satoshi Kataokad787f692013-10-26 04:44:21 +09001321 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1322 enabledInputMethodAndSubtypes.put(
1323 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1324 }
1325 return enabledInputMethodAndSubtypes;
1326 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001327 }
Yohei Yukawa174843a2015-06-26 18:02:54 -07001328
1329 // For spell checker service manager.
1330 // TODO: Should we have TextServicesUtils.java?
1331 private static final Locale LOCALE_EN_US = new Locale("en", "US");
1332 private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
1333
1334 /**
1335 * Returns a list of {@link Locale} in the order of appropriateness for the default spell
1336 * checker service.
1337 *
1338 * <p>If the system language is English, and the region is also explicitly specified in the
1339 * system locale, the following fallback order will be applied.</p>
1340 * <ul>
1341 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1342 * <li>(system-locale-language, system-locale-region)</li>
1343 * <li>("en", "US")</li>
1344 * <li>("en", "GB")</li>
1345 * <li>("en")</li>
1346 * </ul>
1347 *
1348 * <p>If the system language is English, but no region is specified in the system locale,
1349 * the following fallback order will be applied.</p>
1350 * <ul>
1351 * <li>("en")</li>
1352 * <li>("en", "US")</li>
1353 * <li>("en", "GB")</li>
1354 * </ul>
1355 *
1356 * <p>If the system language is not English, the following fallback order will be applied.</p>
1357 * <ul>
1358 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1359 * <li>(system-locale-language, system-locale-region) (if exists)</li>
1360 * <li>(system-locale-language) (if exists)</li>
1361 * <li>("en", "US")</li>
1362 * <li>("en", "GB")</li>
1363 * <li>("en")</li>
1364 * </ul>
1365 *
1366 * @param systemLocale the current system locale to be taken into consideration.
1367 * @return a list of {@link Locale}. The first one is considered to be most appropriate.
1368 */
1369 @VisibleForTesting
1370 public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
1371 @Nullable final Locale systemLocale) {
1372 final Locale systemLocaleLanguageCountryVariant;
1373 final Locale systemLocaleLanguageCountry;
1374 final Locale systemLocaleLanguage;
1375 if (systemLocale != null) {
1376 final String language = systemLocale.getLanguage();
1377 final boolean hasLanguage = !TextUtils.isEmpty(language);
1378 final String country = systemLocale.getCountry();
1379 final boolean hasCountry = !TextUtils.isEmpty(country);
1380 final String variant = systemLocale.getVariant();
1381 final boolean hasVariant = !TextUtils.isEmpty(variant);
1382 if (hasLanguage && hasCountry && hasVariant) {
1383 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
1384 } else {
1385 systemLocaleLanguageCountryVariant = null;
1386 }
1387 if (hasLanguage && hasCountry) {
1388 systemLocaleLanguageCountry = new Locale(language, country);
1389 } else {
1390 systemLocaleLanguageCountry = null;
1391 }
1392 if (hasLanguage) {
1393 systemLocaleLanguage = new Locale(language);
1394 } else {
1395 systemLocaleLanguage = null;
1396 }
1397 } else {
1398 systemLocaleLanguageCountryVariant = null;
1399 systemLocaleLanguageCountry = null;
1400 systemLocaleLanguage = null;
1401 }
1402
1403 final ArrayList<Locale> locales = new ArrayList<>();
1404 if (systemLocaleLanguageCountryVariant != null) {
1405 locales.add(systemLocaleLanguageCountryVariant);
1406 }
1407
1408 if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
1409 if (systemLocaleLanguageCountry != null) {
1410 // If the system language is English, and the region is also explicitly specified,
1411 // following fallback order will be applied.
1412 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
1413 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
1414 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
1415 // - en
1416 if (systemLocaleLanguageCountry != null) {
1417 locales.add(systemLocaleLanguageCountry);
1418 }
1419 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
1420 locales.add(LOCALE_EN_US);
1421 }
1422 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
1423 locales.add(LOCALE_EN_GB);
1424 }
1425 locales.add(Locale.ENGLISH);
1426 } else {
1427 // If the system language is English, but no region is specified, following
1428 // fallback order will be applied.
1429 // - en
1430 // - en_US
1431 // - en_GB
1432 locales.add(Locale.ENGLISH);
1433 locales.add(LOCALE_EN_US);
1434 locales.add(LOCALE_EN_GB);
1435 }
1436 } else {
1437 // If the system language is not English, the fallback order will be
1438 // - systemLocaleLanguageCountry [if non-null]
1439 // - systemLocaleLanguage [if non-null]
1440 // - en_US
1441 // - en_GB
1442 // - en
1443 if (systemLocaleLanguageCountry != null) {
1444 locales.add(systemLocaleLanguageCountry);
1445 }
1446 if (systemLocaleLanguage != null) {
1447 locales.add(systemLocaleLanguage);
1448 }
1449 locales.add(LOCALE_EN_US);
1450 locales.add(LOCALE_EN_GB);
1451 locales.add(Locale.ENGLISH);
1452 }
1453 return locales;
1454 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001455}