blob: be122a0048db39a03238dd84528e31e665d37e17 [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;
30import android.provider.Settings.SettingNotFoundException;
31import android.text.TextUtils;
Seigo Nonaka2028dda2015-07-06 17:41:24 +090032import android.text.TextUtils.SimpleStringSplitter;
33import android.util.ArrayMap;
34import android.util.ArraySet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090035import android.util.Pair;
36import android.util.Slog;
37import android.view.inputmethod.InputMethodInfo;
38import android.view.inputmethod.InputMethodSubtype;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +090039import android.view.textservice.SpellCheckerInfo;
40import android.view.textservice.TextServicesManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090041
Yohei Yukawae72d1c82015-02-20 20:55:21 +090042import com.android.internal.annotations.VisibleForTesting;
43
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090044import java.util.ArrayList;
Yohei Yukawab21220e2014-11-01 21:04:30 +090045import java.util.Arrays;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090046import java.util.HashMap;
Yohei Yukawab21220e2014-11-01 21:04:30 +090047import java.util.LinkedHashSet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090048import java.util.List;
49import java.util.Locale;
50
51/**
52 * InputMethodManagerUtils contains some static methods that provides IME informations.
53 * This methods are supposed to be used in both the framework and the Settings application.
54 */
55public class InputMethodUtils {
56 public static final boolean DEBUG = false;
57 public static final int NOT_A_SUBTYPE_ID = -1;
Yohei Yukawa68c860b2014-09-13 22:03:37 +090058 public static final String SUBTYPE_MODE_ANY = null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090059 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
60 public static final String SUBTYPE_MODE_VOICE = "voice";
61 private static final String TAG = "InputMethodUtils";
62 private static final Locale ENGLISH_LOCALE = new Locale("en");
63 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
64 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
65 "EnabledWhenDefaultIsNotAsciiCapable";
66 private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
Seigo Nonakace2c7842015-08-17 08:47:36 -070067
68 // The string for enabled input method is saved as follows:
69 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
Seigo Nonaka2028dda2015-07-06 17:41:24 +090070 private static final char INPUT_METHOD_SEPARATOR = ':';
71 private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
Yohei Yukawadc489242014-09-14 12:01:59 +090072 /**
73 * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
74 * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
75 * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
76 * doesn't automatically match {@code Locale("en", "IN")}.
77 */
78 private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
79 Locale.ENGLISH, // "en"
80 Locale.US, // "en_US"
81 Locale.UK, // "en_GB"
82 };
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090083
84 private InputMethodUtils() {
85 // This utility class is not publicly instantiable.
86 }
87
Satoshi Kataoka0766eb02013-07-31 18:30:13 +090088 // ----------------------------------------------------------------------
89 // Utilities for debug
Satoshi Kataoka87c29142013-07-31 23:11:54 +090090 public static String getApiCallStack() {
91 String apiCallStack = "";
92 try {
93 throw new RuntimeException();
94 } catch (RuntimeException e) {
95 final StackTraceElement[] frames = e.getStackTrace();
96 for (int j = 1; j < frames.length; ++j) {
97 final String tempCallStack = frames[j].toString();
98 if (TextUtils.isEmpty(apiCallStack)) {
99 // Overwrite apiCallStack if it's empty
100 apiCallStack = tempCallStack;
101 } else if (tempCallStack.indexOf("Transact(") < 0) {
102 // Overwrite apiCallStack if it's not a binder call
103 apiCallStack = tempCallStack;
104 } else {
105 break;
106 }
107 }
108 }
109 return apiCallStack;
110 }
Satoshi Kataoka0766eb02013-07-31 18:30:13 +0900111 // ----------------------------------------------------------------------
112
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900113 public static boolean isSystemIme(InputMethodInfo inputMethod) {
114 return (inputMethod.getServiceInfo().applicationInfo.flags
115 & ApplicationInfo.FLAG_SYSTEM) != 0;
116 }
117
Yohei Yukawa9c83ff42015-03-12 15:31:25 +0900118 public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900119 final Context context, final boolean checkDefaultAttribute,
120 @Nullable final Locale requiredLocale, final boolean checkCountry,
121 final String requiredSubtypeMode) {
122 if (!isSystemIme(imi)) {
123 return false;
124 }
125 if (checkDefaultAttribute && !imi.isDefault(context)) {
126 return false;
127 }
128 if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
129 return false;
130 }
131 return true;
132 }
133
134 @Nullable
Yohei Yukawadc489242014-09-14 12:01:59 +0900135 public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis,
136 final Context context) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900137 // At first, find the fallback locale from the IMEs that are declared as "default" in the
138 // current locale. Note that IME developers can declare an IME as "default" only for
139 // some particular locales but "not default" for other locales.
Yohei Yukawadc489242014-09-14 12:01:59 +0900140 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
141 for (int i = 0; i < imis.size(); ++i) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900142 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
143 true /* checkDefaultAttribute */, fallbackLocale,
144 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900145 return fallbackLocale;
146 }
147 }
148 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900149 // If no fallback locale is found in the above condition, find fallback locales regardless
150 // of the "default" attribute as a last resort.
151 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
152 for (int i = 0; i < imis.size(); ++i) {
153 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
154 false /* checkDefaultAttribute */, fallbackLocale,
155 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
156 return fallbackLocale;
157 }
158 }
159 }
160 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
Yohei Yukawadc489242014-09-14 12:01:59 +0900161 return null;
162 }
163
Yohei Yukawab21220e2014-11-01 21:04:30 +0900164 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi,
165 final Context context, final boolean checkDefaultAttribute) {
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900166 if (!isSystemIme(imi)) {
167 return false;
168 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900169 if (checkDefaultAttribute && !imi.isDefault(context)) {
170 return false;
171 }
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900172 if (!imi.isAuxiliaryIme()) {
173 return false;
174 }
175 final int subtypeCount = imi.getSubtypeCount();
176 for (int i = 0; i < subtypeCount; ++i) {
177 final InputMethodSubtype s = imi.getSubtypeAt(i);
178 if (s.overridesImplicitlyEnabledSubtype()) {
179 return true;
180 }
181 }
182 return false;
183 }
184
Yohei Yukawadc489242014-09-14 12:01:59 +0900185 public static Locale getSystemLocaleFromContext(final Context context) {
186 try {
187 return context.getResources().getConfiguration().locale;
188 } catch (Resources.NotFoundException ex) {
189 return null;
190 }
191 }
192
Yohei Yukawab21220e2014-11-01 21:04:30 +0900193 private static final class InputMethodListBuilder {
194 // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
195 // order can have non-trivial effect in the call sites.
196 @NonNull
197 private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
Yohei Yukawadc489242014-09-14 12:01:59 +0900198
Yohei Yukawab21220e2014-11-01 21:04:30 +0900199 public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis,
200 final Context context, final boolean checkDefaultAttribute,
201 @Nullable final Locale locale, final boolean checkCountry,
202 final String requiredSubtypeMode) {
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900203 for (int i = 0; i < imis.size(); ++i) {
204 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900205 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
206 checkCountry, requiredSubtypeMode)) {
207 mInputMethodSet.add(imi);
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900208 }
209 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900210 return this;
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900211 }
212
Yohei Yukawab21220e2014-11-01 21:04:30 +0900213 // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
214 // documented more clearly.
215 public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis,
216 final Context context) {
217 // If one or more auxiliary input methods are available, OK to stop populating the list.
218 for (final InputMethodInfo imi : mInputMethodSet) {
219 if (imi.isAuxiliaryIme()) {
220 return this;
221 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900222 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900223 boolean added = false;
Yohei Yukawadc489242014-09-14 12:01:59 +0900224 for (int i = 0; i < imis.size(); ++i) {
225 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900226 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
227 true /* checkDefaultAttribute */)) {
228 mInputMethodSet.add(imi);
229 added = true;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900230 }
231 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900232 if (added) {
233 return this;
234 }
235 for (int i = 0; i < imis.size(); ++i) {
236 final InputMethodInfo imi = imis.get(i);
237 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
238 false /* checkDefaultAttribute */)) {
239 mInputMethodSet.add(imi);
240 }
241 }
242 return this;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900243 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900244
Yohei Yukawab21220e2014-11-01 21:04:30 +0900245 public boolean isEmpty() {
246 return mInputMethodSet.isEmpty();
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900247 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900248
249 @NonNull
250 public ArrayList<InputMethodInfo> build() {
251 return new ArrayList<>(mInputMethodSet);
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900252 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900253 }
254
Yohei Yukawab21220e2014-11-01 21:04:30 +0900255 private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale(
256 final ArrayList<InputMethodInfo> imis, final Context context,
257 @Nullable final Locale fallbackLocale) {
258 // Before the system becomes ready, we pick up at least one keyboard in the following order.
259 // The first user (device owner) falls into this category.
260 // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
261 // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
262 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
263 // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
264 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
265
266 final InputMethodListBuilder builder = new InputMethodListBuilder();
267 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
268 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
269 if (!builder.isEmpty()) {
270 return builder;
Yohei Yukawadc489242014-09-14 12:01:59 +0900271 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900272 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
273 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
274 if (!builder.isEmpty()) {
275 return builder;
276 }
277 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
278 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
279 if (!builder.isEmpty()) {
280 return builder;
281 }
282 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
283 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
284 if (!builder.isEmpty()) {
285 return builder;
286 }
287 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
288 + " fallbackLocale=" + fallbackLocale);
289 return builder;
290 }
291
292 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
293 final ArrayList<InputMethodInfo> imis, final Context context,
294 @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
295 // Once the system becomes ready, we pick up at least one keyboard in the following order.
296 // Secondary users fall into this category in general.
297 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
298 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
299 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
300 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
301 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
302 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
303 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
304
305 final InputMethodListBuilder builder = new InputMethodListBuilder();
306 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
307 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
308 if (!builder.isEmpty()) {
309 return builder;
310 }
311 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
312 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
313 if (!builder.isEmpty()) {
314 return builder;
315 }
316 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
317 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
318 if (!builder.isEmpty()) {
319 return builder;
320 }
321 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
322 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
323 if (!builder.isEmpty()) {
324 return builder;
325 }
326 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
327 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
328 if (!builder.isEmpty()) {
329 return builder;
330 }
331 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
332 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
333 if (!builder.isEmpty()) {
334 return builder;
335 }
336 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
337 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
338 return builder;
339 }
340
341 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
342 final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) {
343 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
344 if (!isSystemReady) {
345 // When the system is not ready, the system locale is not stable and reliable. Hence
346 // we will pick up IMEs that support software keyboard based on the fallback locale.
347 // Also pick up suitable IMEs regardless of the software keyboard support.
348 // (e.g. Voice IMEs)
349 return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale)
350 .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
351 true /* checkCountry */, SUBTYPE_MODE_ANY)
352 .build();
353 }
354
355 // When the system is ready, we will primarily rely on the system locale, but also keep
356 // relying on the fallback locale as a last resort.
357 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
358 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
359 // subtype)
360 final Locale systemLocale = getSystemLocaleFromContext(context);
361 return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
362 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
363 true /* checkCountry */, SUBTYPE_MODE_ANY)
364 .fillAuxiliaryImes(imis, context)
365 .build();
Yohei Yukawadc489242014-09-14 12:01:59 +0900366 }
367
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900368 public static Locale constructLocaleFromString(String localeStr) {
369 if (TextUtils.isEmpty(localeStr)) {
370 return null;
371 }
372 // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
373 String[] localeParams = localeStr.split("_", 3);
Yohei Yukawaed65bc02015-12-02 18:22:41 -0800374 if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
375 // Convert a locale whose language is "tl" to one whose language is "fil".
376 // For example, "tl_PH" will get converted to "fil_PH".
377 // Versions of Android earlier than Lollipop did not support three letter language
378 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
379 // On Lollipop and above, the current three letter version must be used.
380 localeParams[0] = "fil";
381 }
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900382 // The length of localeStr is guaranteed to always return a 1 <= value <= 3
383 // because localeStr is not empty.
384 if (localeParams.length == 1) {
385 return new Locale(localeParams[0]);
386 } else if (localeParams.length == 2) {
387 return new Locale(localeParams[0], localeParams[1]);
388 } else if (localeParams.length == 3) {
389 return new Locale(localeParams[0], localeParams[1], localeParams[2]);
390 }
391 return null;
392 }
393
Yohei Yukawadc489242014-09-14 12:01:59 +0900394 public static boolean containsSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900395 @Nullable final Locale locale, final boolean checkCountry, final String mode) {
396 if (locale == null) {
397 return false;
398 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900399 final int N = imi.getSubtypeCount();
400 for (int i = 0; i < N; ++i) {
401 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900402 if (checkCountry) {
Yohei Yukawa92280cd2015-06-02 16:50:14 -0700403 final Locale subtypeLocale = subtype.getLocaleObject();
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900404 if (subtypeLocale == null ||
405 !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
406 !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900407 continue;
408 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900409 } else {
410 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
411 subtype.getLocale()));
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900412 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900413 continue;
414 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900415 }
416 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
417 mode.equalsIgnoreCase(subtype.getMode())) {
418 return true;
419 }
420 }
421 return false;
422 }
423
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900424 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700425 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900426 final int subtypeCount = imi.getSubtypeCount();
427 for (int i = 0; i < subtypeCount; ++i) {
428 subtypes.add(imi.getSubtypeAt(i));
429 }
430 return subtypes;
431 }
432
433 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
434 InputMethodInfo imi, String mode) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700435 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900436 final int subtypeCount = imi.getSubtypeCount();
437 for (int i = 0; i < subtypeCount; ++i) {
438 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
439 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
440 subtypes.add(subtype);
441 }
442 }
443 return subtypes;
444 }
445
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900446 public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900447 if (enabledImes == null || enabledImes.isEmpty()) {
448 return null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900449 }
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900450 // We'd prefer to fall back on a system IME, since that is safer.
451 int i = enabledImes.size();
452 int firstFoundSystemIme = -1;
453 while (i > 0) {
454 i--;
455 final InputMethodInfo imi = enabledImes.get(i);
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900456 if (imi.isAuxiliaryIme()) {
457 continue;
458 }
459 if (InputMethodUtils.isSystemIme(imi)
460 && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
461 SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900462 return imi;
463 }
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900464 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900465 firstFoundSystemIme = i;
466 }
467 }
468 return enabledImes.get(Math.max(firstFoundSystemIme, 0));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900469 }
470
471 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
472 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
473 }
474
475 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
476 if (imi != null) {
477 final int subtypeCount = imi.getSubtypeCount();
478 for (int i = 0; i < subtypeCount; ++i) {
479 InputMethodSubtype ims = imi.getSubtypeAt(i);
480 if (subtypeHashCode == ims.hashCode()) {
481 return i;
482 }
483 }
484 }
485 return NOT_A_SUBTYPE_ID;
486 }
487
Yohei Yukawae72d1c82015-02-20 20:55:21 +0900488 @VisibleForTesting
489 public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900490 Resources res, InputMethodInfo imi) {
491 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
492 final String systemLocale = res.getConfiguration().locale.toString();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700493 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100494 final String systemLanguage = res.getConfiguration().locale.getLanguage();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700495 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900496 final int N = subtypes.size();
497 for (int i = 0; i < N; ++i) {
498 // scan overriding implicitly enabled subtypes.
499 InputMethodSubtype subtype = subtypes.get(i);
500 if (subtype.overridesImplicitlyEnabledSubtype()) {
501 final String mode = subtype.getMode();
502 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
503 applicableModeAndSubtypesMap.put(mode, subtype);
504 }
505 }
506 }
507 if (applicableModeAndSubtypesMap.size() > 0) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700508 return new ArrayList<>(applicableModeAndSubtypesMap.values());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900509 }
510 for (int i = 0; i < N; ++i) {
511 final InputMethodSubtype subtype = subtypes.get(i);
512 final String locale = subtype.getLocale();
513 final String mode = subtype.getMode();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100514 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900515 // When system locale starts with subtype's locale, that subtype will be applicable
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100516 // for system locale. We need to make sure the languages are the same, to prevent
517 // locales like "fil" (Filipino) being matched by "fi" (Finnish).
518 //
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900519 // For instance, it's clearly applicable for cases like system locale = en_US and
520 // subtype = en, but it is not necessarily considered applicable for cases like system
521 // locale = en and subtype = en_US.
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100522 //
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900523 // We just call systemLocale.startsWith(locale) in this function because there is no
524 // need to find applicable subtypes aggressively unlike
525 // findLastResortApplicableSubtypeLocked.
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100526 //
527 // TODO: This check is broken. It won't take scripts into account and doesn't
528 // account for the mandatory conversions performed by Locale#toString.
529 if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900530 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
531 // If more applicable subtypes are contained, skip.
532 if (applicableSubtype != null) {
533 if (systemLocale.equals(applicableSubtype.getLocale())) continue;
534 if (!systemLocale.equals(locale)) continue;
535 }
536 applicableModeAndSubtypesMap.put(mode, subtype);
537 }
538 }
539 final InputMethodSubtype keyboardSubtype
540 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700541 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900542 applicableModeAndSubtypesMap.values());
543 if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
544 for (int i = 0; i < N; ++i) {
545 final InputMethodSubtype subtype = subtypes.get(i);
546 final String mode = subtype.getMode();
547 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
548 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
549 applicableSubtypes.add(subtype);
550 }
551 }
552 }
553 if (keyboardSubtype == null) {
554 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
555 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
556 if (lastResortKeyboardSubtype != null) {
557 applicableSubtypes.add(lastResortKeyboardSubtype);
558 }
559 }
560 return applicableSubtypes;
561 }
562
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900563 /**
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100564 * Returns the language component of a given locale string.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900565 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100566 */
Tadashi G. Takaoka77cbcb62014-07-12 16:08:20 +0900567 public static String getLanguageFromLocaleString(String locale) {
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100568 final int idx = locale.indexOf('_');
569 if (idx < 0) {
570 return locale;
571 } else {
572 return locale.substring(0, idx);
573 }
574 }
575
576 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900577 * If there are no selected subtypes, tries finding the most applicable one according to the
578 * given locale.
579 * @param subtypes this function will search the most applicable subtype in subtypes
580 * @param mode subtypes will be filtered by mode
581 * @param locale subtypes will be filtered by locale
582 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
583 * it will return the first subtype matched with mode
584 * @return the most applicable subtypeId
585 */
586 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
587 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
588 boolean canIgnoreLocaleAsLastResort) {
589 if (subtypes == null || subtypes.size() == 0) {
590 return null;
591 }
592 if (TextUtils.isEmpty(locale)) {
593 locale = res.getConfiguration().locale.toString();
594 }
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100595 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900596 boolean partialMatchFound = false;
597 InputMethodSubtype applicableSubtype = null;
598 InputMethodSubtype firstMatchedModeSubtype = null;
599 final int N = subtypes.size();
600 for (int i = 0; i < N; ++i) {
601 InputMethodSubtype subtype = subtypes.get(i);
602 final String subtypeLocale = subtype.getLocale();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100603 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900604 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
605 // and all subtypes with all modes can be candidates.
606 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
607 if (firstMatchedModeSubtype == null) {
608 firstMatchedModeSubtype = subtype;
609 }
610 if (locale.equals(subtypeLocale)) {
611 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
612 applicableSubtype = subtype;
613 break;
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100614 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900615 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
616 applicableSubtype = subtype;
617 partialMatchFound = true;
618 }
619 }
620 }
621
622 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
623 return firstMatchedModeSubtype;
624 }
625
626 // The first subtype applicable to the system locale will be defined as the most applicable
627 // subtype.
628 if (DEBUG) {
629 if (applicableSubtype != null) {
630 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
631 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
632 }
633 }
634 return applicableSubtype;
635 }
636
637 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
638 if (subtype == null) return true;
639 return !subtype.isAuxiliary();
640 }
641
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900642 public static void setNonSelectedSystemImesDisabledUntilUsed(
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700643 IPackageManager packageManager, List<InputMethodInfo> enabledImis,
644 int userId, String callingPackage) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900645 if (DEBUG) {
646 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
647 }
648 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
649 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
650 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
651 return;
652 }
653 // Only the current spell checker should be treated as an enabled one.
654 final SpellCheckerInfo currentSpellChecker =
655 TextServicesManager.getInstance().getCurrentSpellChecker();
656 for (final String packageName : systemImesDisabledUntilUsed) {
657 if (DEBUG) {
658 Slog.d(TAG, "check " + packageName);
659 }
660 boolean enabledIme = false;
661 for (int j = 0; j < enabledImis.size(); ++j) {
662 final InputMethodInfo imi = enabledImis.get(j);
663 if (packageName.equals(imi.getPackageName())) {
664 enabledIme = true;
665 break;
666 }
667 }
668 if (enabledIme) {
669 // enabled ime. skip
670 continue;
671 }
672 if (currentSpellChecker != null
673 && packageName.equals(currentSpellChecker.getPackageName())) {
674 // enabled spell checker. skip
675 if (DEBUG) {
676 Slog.d(TAG, packageName + " is the current spell checker. skip");
677 }
678 continue;
679 }
680 ApplicationInfo ai = null;
681 try {
682 ai = packageManager.getApplicationInfo(packageName,
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700683 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
684 } catch (RemoteException e) {
685 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
686 + " userId=" + userId, e);
687 continue;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900688 }
689 if (ai == null) {
690 // No app found for packageName
691 continue;
692 }
693 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
694 if (!isSystemPackage) {
695 continue;
696 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700697 setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900698 }
699 }
700
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700701 private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
702 int userId, String callingPackage) {
703 final int state;
704 try {
705 state = packageManager.getApplicationEnabledSetting(packageName, userId);
706 } catch (RemoteException e) {
707 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
708 + " userId=" + userId, e);
709 return;
710 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900711 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
712 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
713 if (DEBUG) {
714 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
715 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700716 try {
717 packageManager.setApplicationEnabledSetting(packageName,
718 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
719 0 /* newState */, userId, callingPackage);
720 } catch (RemoteException e) {
721 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
722 + " userId=" + userId + " callingPackage=" + callingPackage, e);
723 return;
724 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900725 } else {
726 if (DEBUG) {
727 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
728 }
729 }
730 }
731
Satoshi Kataokab2827262013-07-04 19:43:14 +0900732 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
733 InputMethodSubtype subtype) {
734 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
735 return subtype != null
736 ? TextUtils.concat(subtype.getDisplayName(context,
737 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
738 (TextUtils.isEmpty(imiLabel) ?
739 "" : " - " + imiLabel))
740 : imiLabel;
741 }
742
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900743 /**
Yohei Yukawae63b5fa2014-09-19 13:14:55 +0900744 * Returns true if a package name belongs to a UID.
745 *
746 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
747 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
748 * @param uid the UID to be validated.
749 * @param packageName the package name.
750 * @return {@code true} if the package name belongs to the UID.
751 */
752 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
753 final int uid, final String packageName) {
754 try {
755 appOpsManager.checkPackage(uid, packageName);
756 return true;
757 } catch (SecurityException e) {
758 return false;
759 }
760 }
761
762 /**
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900763 * Parses the setting stored input methods and subtypes string value.
764 *
765 * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
766 * @return Map from input method ID to set of input method subtypes IDs.
767 */
768 @VisibleForTesting
769 public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
770 @Nullable final String inputMethodsAndSubtypesString) {
771
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800772 final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900773 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
774 return imeMap;
775 }
776
777 final SimpleStringSplitter typeSplitter =
778 new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
779 final SimpleStringSplitter subtypeSplitter =
780 new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
781
782 List<Pair<String, ArrayList<String>>> allImeSettings =
783 InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
784 typeSplitter,
785 subtypeSplitter);
786 for (Pair<String, ArrayList<String>> ime : allImeSettings) {
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800787 ArraySet<String> subtypes = new ArraySet<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900788 if (ime.second != null) {
789 subtypes.addAll(ime.second);
790 }
791 imeMap.put(ime.first, subtypes);
792 }
793 return imeMap;
794 }
795
Seigo Nonaka2a099bc2015-08-14 19:29:45 -0700796 @NonNull
797 public static String buildInputMethodsAndSubtypesString(
798 @NonNull final ArrayMap<String, ArraySet<String>> map) {
799 // we want to use the canonical InputMethodSettings implementation,
800 // so we convert data structures first.
801 List<Pair<String, ArrayList<String>>> imeMap = new ArrayList<>(4);
802 for (ArrayMap.Entry<String, ArraySet<String>> entry : map.entrySet()) {
803 final String imeName = entry.getKey();
804 final ArraySet<String> subtypeSet = entry.getValue();
805 final ArrayList<String> subtypes = new ArrayList<>(2);
806 if (subtypeSet != null) {
807 subtypes.addAll(subtypeSet);
808 }
809 imeMap.add(new Pair<>(imeName, subtypes));
810 }
811 return InputMethodSettings.buildInputMethodsSettingString(imeMap);
812 }
813
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900814 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900815 * Utility class for putting and getting settings for InputMethod
816 * TODO: Move all putters and getters of settings to this class.
817 */
818 public static class InputMethodSettings {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900819 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700820 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900821
822 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700823 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900824
825 private final Resources mRes;
826 private final ContentResolver mResolver;
827 private final HashMap<String, InputMethodInfo> mMethodMap;
828 private final ArrayList<InputMethodInfo> mMethodList;
829
830 private String mEnabledInputMethodsStrCache;
831 private int mCurrentUserId;
Kenny Guy2a764942014-04-02 13:29:20 +0100832 private int[] mCurrentProfileIds = new int[0];
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900833
834 private static void buildEnabledInputMethodsSettingString(
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700835 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
836 builder.append(ime.first);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900837 // Inputmethod and subtypes are saved in the settings as follows:
838 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700839 for (String subtypeId: ime.second) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700840 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900841 }
842 }
843
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700844 public static String buildInputMethodsSettingString(
845 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
846 final StringBuilder b = new StringBuilder();
847 boolean needsSeparator = false;
848 for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
849 if (needsSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700850 b.append(INPUT_METHOD_SEPARATOR);
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700851 }
852 buildEnabledInputMethodsSettingString(b, ime);
853 needsSeparator = true;
854 }
855 return b.toString();
856 }
857
858 public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
859 String enabledInputMethodsStr,
860 TextUtils.SimpleStringSplitter inputMethodSplitter,
861 TextUtils.SimpleStringSplitter subtypeSplitter) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700862 ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700863 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
864 return imsList;
865 }
866 inputMethodSplitter.setString(enabledInputMethodsStr);
867 while (inputMethodSplitter.hasNext()) {
868 String nextImsStr = inputMethodSplitter.next();
869 subtypeSplitter.setString(nextImsStr);
870 if (subtypeSplitter.hasNext()) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700871 ArrayList<String> subtypeHashes = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700872 // The first element is ime id.
873 String imeId = subtypeSplitter.next();
874 while (subtypeSplitter.hasNext()) {
875 subtypeHashes.add(subtypeSplitter.next());
876 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700877 imsList.add(new Pair<>(imeId, subtypeHashes));
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700878 }
879 }
880 return imsList;
881 }
882
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900883 public InputMethodSettings(
884 Resources res, ContentResolver resolver,
885 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
886 int userId) {
887 setCurrentUserId(userId);
888 mRes = res;
889 mResolver = resolver;
890 mMethodMap = methodMap;
891 mMethodList = methodList;
892 }
893
894 public void setCurrentUserId(int userId) {
895 if (DEBUG) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900896 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900897 }
898 // IMMS settings are kept per user, so keep track of current user
899 mCurrentUserId = userId;
900 }
901
Kenny Guy2a764942014-04-02 13:29:20 +0100902 public void setCurrentProfileIds(int[] currentProfileIds) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800903 synchronized (this) {
Kenny Guy2a764942014-04-02 13:29:20 +0100904 mCurrentProfileIds = currentProfileIds;
Amith Yamasani734983f2014-03-04 16:48:05 -0800905 }
906 }
907
Kenny Guy2a764942014-04-02 13:29:20 +0100908 public boolean isCurrentProfile(int userId) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800909 synchronized (this) {
Kenny Guyf4824a02014-04-02 19:17:41 +0100910 if (userId == mCurrentUserId) return true;
Kenny Guy2a764942014-04-02 13:29:20 +0100911 for (int i = 0; i < mCurrentProfileIds.length; i++) {
912 if (userId == mCurrentProfileIds[i]) return true;
Amith Yamasani734983f2014-03-04 16:48:05 -0800913 }
914 return false;
915 }
916 }
917
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900918 public List<InputMethodInfo> getEnabledInputMethodListLocked() {
919 return createEnabledInputMethodListLocked(
920 getEnabledInputMethodsAndSubtypeListLocked());
921 }
922
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900923 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
924 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
925 List<InputMethodSubtype> enabledSubtypes =
926 getEnabledInputMethodSubtypeListLocked(imi);
927 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
928 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
929 context.getResources(), imi);
930 }
931 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
932 }
933
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +0900934 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900935 InputMethodInfo imi) {
936 List<Pair<String, ArrayList<String>>> imsList =
937 getEnabledInputMethodsAndSubtypeListLocked();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700938 ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900939 if (imi != null) {
940 for (Pair<String, ArrayList<String>> imsPair : imsList) {
941 InputMethodInfo info = mMethodMap.get(imsPair.first);
942 if (info != null && info.getId().equals(imi.getId())) {
943 final int subtypeCount = info.getSubtypeCount();
944 for (int i = 0; i < subtypeCount; ++i) {
945 InputMethodSubtype ims = info.getSubtypeAt(i);
946 for (String s: imsPair.second) {
947 if (String.valueOf(ims.hashCode()).equals(s)) {
948 enabledSubtypes.add(ims);
949 }
950 }
951 }
952 break;
953 }
954 }
955 }
956 return enabledSubtypes;
957 }
958
959 // At the initial boot, the settings for input methods are not set,
960 // so we need to enable IME in that case.
961 public void enableAllIMEsIfThereIsNoEnabledIME() {
962 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
963 StringBuilder sb = new StringBuilder();
964 final int N = mMethodList.size();
965 for (int i = 0; i < N; i++) {
966 InputMethodInfo imi = mMethodList.get(i);
967 Slog.i(TAG, "Adding: " + imi.getId());
968 if (i > 0) sb.append(':');
969 sb.append(imi.getId());
970 }
971 putEnabledInputMethodsStr(sb.toString());
972 }
973 }
974
975 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700976 return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
977 mInputMethodSplitter,
978 mSubtypeSplitter);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900979 }
980
981 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
982 if (reloadInputMethodStr) {
983 getEnabledInputMethodsStr();
984 }
985 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
986 // Add in the newly enabled input method.
987 putEnabledInputMethodsStr(id);
988 } else {
989 putEnabledInputMethodsStr(
Seigo Nonakace2c7842015-08-17 08:47:36 -0700990 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900991 }
992 }
993
994 /**
995 * Build and put a string of EnabledInputMethods with removing specified Id.
996 * @return the specified id was removed or not.
997 */
998 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
999 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
1000 boolean isRemoved = false;
1001 boolean needsAppendSeparator = false;
1002 for (Pair<String, ArrayList<String>> ims: imsList) {
1003 String curId = ims.first;
1004 if (curId.equals(id)) {
1005 // We are disabling this input method, and it is
1006 // currently enabled. Skip it to remove from the
1007 // new list.
1008 isRemoved = true;
1009 } else {
1010 if (needsAppendSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001011 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001012 } else {
1013 needsAppendSeparator = true;
1014 }
1015 buildEnabledInputMethodsSettingString(builder, ims);
1016 }
1017 }
1018 if (isRemoved) {
1019 // Update the setting with the new list of input methods.
1020 putEnabledInputMethodsStr(builder.toString());
1021 }
1022 return isRemoved;
1023 }
1024
1025 private List<InputMethodInfo> createEnabledInputMethodListLocked(
1026 List<Pair<String, ArrayList<String>>> imsList) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001027 final ArrayList<InputMethodInfo> res = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001028 for (Pair<String, ArrayList<String>> ims: imsList) {
1029 InputMethodInfo info = mMethodMap.get(ims.first);
1030 if (info != null) {
1031 res.add(info);
1032 }
1033 }
1034 return res;
1035 }
1036
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001037 private void putEnabledInputMethodsStr(String str) {
1038 Settings.Secure.putStringForUser(
1039 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId);
1040 mEnabledInputMethodsStrCache = str;
1041 if (DEBUG) {
1042 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1043 }
1044 }
1045
Dianne Hackbornfd7aded2013-01-22 17:10:23 -08001046 public String getEnabledInputMethodsStr() {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001047 mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser(
1048 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId);
1049 if (DEBUG) {
1050 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1051 + ", " + mCurrentUserId);
1052 }
1053 return mEnabledInputMethodsStrCache;
1054 }
1055
1056 private void saveSubtypeHistory(
1057 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1058 StringBuilder builder = new StringBuilder();
1059 boolean isImeAdded = false;
1060 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001061 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001062 newSubtypeId);
1063 isImeAdded = true;
1064 }
1065 for (Pair<String, String> ime: savedImes) {
1066 String imeId = ime.first;
1067 String subtypeId = ime.second;
1068 if (TextUtils.isEmpty(subtypeId)) {
1069 subtypeId = NOT_A_SUBTYPE_ID_STR;
1070 }
1071 if (isImeAdded) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001072 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001073 } else {
1074 isImeAdded = true;
1075 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001076 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001077 subtypeId);
1078 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001079 // Remove the last INPUT_METHOD_SEPARATOR
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001080 putSubtypeHistoryStr(builder.toString());
1081 }
1082
1083 private void addSubtypeToHistory(String imeId, String subtypeId) {
1084 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1085 for (Pair<String, String> ime: subtypeHistory) {
1086 if (ime.first.equals(imeId)) {
1087 if (DEBUG) {
1088 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1089 + ime.second);
1090 }
1091 // We should break here
1092 subtypeHistory.remove(ime);
1093 break;
1094 }
1095 }
1096 if (DEBUG) {
1097 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1098 }
1099 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1100 }
1101
1102 private void putSubtypeHistoryStr(String str) {
1103 if (DEBUG) {
1104 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1105 }
1106 Settings.Secure.putStringForUser(
1107 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId);
1108 }
1109
1110 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1111 // Gets the first one from the history
1112 return getLastSubtypeForInputMethodLockedInternal(null);
1113 }
1114
1115 public String getLastSubtypeForInputMethodLocked(String imeId) {
1116 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1117 if (ime != null) {
1118 return ime.second;
1119 } else {
1120 return null;
1121 }
1122 }
1123
1124 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1125 List<Pair<String, ArrayList<String>>> enabledImes =
1126 getEnabledInputMethodsAndSubtypeListLocked();
1127 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1128 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1129 final String imeInTheHistory = imeAndSubtype.first;
1130 // If imeId is empty, returns the first IME and subtype in the history
1131 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1132 final String subtypeInTheHistory = imeAndSubtype.second;
1133 final String subtypeHashCode =
1134 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1135 enabledImes, imeInTheHistory, subtypeInTheHistory);
1136 if (!TextUtils.isEmpty(subtypeHashCode)) {
1137 if (DEBUG) {
1138 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1139 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001140 return new Pair<>(imeInTheHistory, subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001141 }
1142 }
1143 }
1144 if (DEBUG) {
1145 Slog.d(TAG, "No enabled IME found in the history");
1146 }
1147 return null;
1148 }
1149
1150 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1151 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1152 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1153 if (enabledIme.first.equals(imeId)) {
1154 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1155 final InputMethodInfo imi = mMethodMap.get(imeId);
1156 if (explicitlyEnabledSubtypes.size() == 0) {
1157 // If there are no explicitly enabled subtypes, applicable subtypes are
1158 // enabled implicitly.
1159 // If IME is enabled and no subtypes are enabled, applicable subtypes
1160 // are enabled implicitly, so needs to treat them to be enabled.
1161 if (imi != null && imi.getSubtypeCount() > 0) {
1162 List<InputMethodSubtype> implicitlySelectedSubtypes =
1163 getImplicitlyApplicableSubtypesLocked(mRes, imi);
1164 if (implicitlySelectedSubtypes != null) {
1165 final int N = implicitlySelectedSubtypes.size();
1166 for (int i = 0; i < N; ++i) {
1167 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1168 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1169 return subtypeHashCode;
1170 }
1171 }
1172 }
1173 }
1174 } else {
1175 for (String s: explicitlyEnabledSubtypes) {
1176 if (s.equals(subtypeHashCode)) {
1177 // If both imeId and subtypeId are enabled, return subtypeId.
1178 try {
1179 final int hashCode = Integer.valueOf(subtypeHashCode);
1180 // Check whether the subtype id is valid or not
1181 if (isValidSubtypeId(imi, hashCode)) {
1182 return s;
1183 } else {
1184 return NOT_A_SUBTYPE_ID_STR;
1185 }
1186 } catch (NumberFormatException e) {
1187 return NOT_A_SUBTYPE_ID_STR;
1188 }
1189 }
1190 }
1191 }
1192 // If imeId was enabled but subtypeId was disabled.
1193 return NOT_A_SUBTYPE_ID_STR;
1194 }
1195 }
1196 // If both imeId and subtypeId are disabled, return null
1197 return null;
1198 }
1199
1200 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001201 ArrayList<Pair<String, String>> imsList = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001202 final String subtypeHistoryStr = getSubtypeHistoryStr();
1203 if (TextUtils.isEmpty(subtypeHistoryStr)) {
1204 return imsList;
1205 }
1206 mInputMethodSplitter.setString(subtypeHistoryStr);
1207 while (mInputMethodSplitter.hasNext()) {
1208 String nextImsStr = mInputMethodSplitter.next();
1209 mSubtypeSplitter.setString(nextImsStr);
1210 if (mSubtypeSplitter.hasNext()) {
1211 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1212 // The first element is ime id.
1213 String imeId = mSubtypeSplitter.next();
1214 while (mSubtypeSplitter.hasNext()) {
1215 subtypeId = mSubtypeSplitter.next();
1216 break;
1217 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001218 imsList.add(new Pair<>(imeId, subtypeId));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001219 }
1220 }
1221 return imsList;
1222 }
1223
1224 private String getSubtypeHistoryStr() {
1225 if (DEBUG) {
1226 Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser(
1227 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId));
1228 }
1229 return Settings.Secure.getStringForUser(
1230 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId);
1231 }
1232
1233 public void putSelectedInputMethod(String imeId) {
1234 if (DEBUG) {
1235 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1236 + mCurrentUserId);
1237 }
1238 Settings.Secure.putStringForUser(
1239 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId);
1240 }
1241
1242 public void putSelectedSubtype(int subtypeId) {
1243 if (DEBUG) {
1244 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1245 + mCurrentUserId);
1246 }
1247 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
1248 subtypeId, mCurrentUserId);
1249 }
1250
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001251 public String getSelectedInputMethod() {
1252 if (DEBUG) {
1253 Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser(
1254 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId)
1255 + ", " + mCurrentUserId);
1256 }
1257 return Settings.Secure.getStringForUser(
1258 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId);
1259 }
1260
1261 public boolean isSubtypeSelected() {
1262 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1263 }
1264
1265 private int getSelectedInputMethodSubtypeHashCode() {
1266 try {
1267 return Settings.Secure.getIntForUser(
1268 mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId);
1269 } catch (SettingNotFoundException e) {
1270 return NOT_A_SUBTYPE_ID;
1271 }
1272 }
1273
Michael Wright7b5a96b2014-08-09 19:28:42 -07001274 public boolean isShowImeWithHardKeyboardEnabled() {
1275 return Settings.Secure.getIntForUser(mResolver,
1276 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0, mCurrentUserId) == 1;
1277 }
1278
1279 public void setShowImeWithHardKeyboard(boolean show) {
1280 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
1281 show ? 1 : 0, mCurrentUserId);
1282 }
1283
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001284 public int getCurrentUserId() {
1285 return mCurrentUserId;
1286 }
1287
1288 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
1289 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1290 if (imi == null) {
1291 return NOT_A_SUBTYPE_ID;
1292 }
1293 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1294 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1295 }
1296
1297 public void saveCurrentInputMethodAndSubtypeToHistory(
1298 String curMethodId, InputMethodSubtype currentSubtype) {
1299 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1300 if (currentSubtype != null) {
1301 subtypeId = String.valueOf(currentSubtype.hashCode());
1302 }
1303 if (canAddToLastInputMethod(currentSubtype)) {
1304 addSubtypeToHistory(curMethodId, subtypeId);
1305 }
1306 }
Satoshi Kataokad787f692013-10-26 04:44:21 +09001307
1308 public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1309 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1310 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001311 new HashMap<>();
Satoshi Kataokad787f692013-10-26 04:44:21 +09001312 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1313 enabledInputMethodAndSubtypes.put(
1314 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1315 }
1316 return enabledInputMethodAndSubtypes;
1317 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001318 }
Yohei Yukawa174843a2015-06-26 18:02:54 -07001319
1320 // For spell checker service manager.
1321 // TODO: Should we have TextServicesUtils.java?
1322 private static final Locale LOCALE_EN_US = new Locale("en", "US");
1323 private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
1324
1325 /**
1326 * Returns a list of {@link Locale} in the order of appropriateness for the default spell
1327 * checker service.
1328 *
1329 * <p>If the system language is English, and the region is also explicitly specified in the
1330 * system locale, the following fallback order will be applied.</p>
1331 * <ul>
1332 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1333 * <li>(system-locale-language, system-locale-region)</li>
1334 * <li>("en", "US")</li>
1335 * <li>("en", "GB")</li>
1336 * <li>("en")</li>
1337 * </ul>
1338 *
1339 * <p>If the system language is English, but no region is specified in the system locale,
1340 * the following fallback order will be applied.</p>
1341 * <ul>
1342 * <li>("en")</li>
1343 * <li>("en", "US")</li>
1344 * <li>("en", "GB")</li>
1345 * </ul>
1346 *
1347 * <p>If the system language is not English, the following fallback order will be applied.</p>
1348 * <ul>
1349 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1350 * <li>(system-locale-language, system-locale-region) (if exists)</li>
1351 * <li>(system-locale-language) (if exists)</li>
1352 * <li>("en", "US")</li>
1353 * <li>("en", "GB")</li>
1354 * <li>("en")</li>
1355 * </ul>
1356 *
1357 * @param systemLocale the current system locale to be taken into consideration.
1358 * @return a list of {@link Locale}. The first one is considered to be most appropriate.
1359 */
1360 @VisibleForTesting
1361 public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
1362 @Nullable final Locale systemLocale) {
1363 final Locale systemLocaleLanguageCountryVariant;
1364 final Locale systemLocaleLanguageCountry;
1365 final Locale systemLocaleLanguage;
1366 if (systemLocale != null) {
1367 final String language = systemLocale.getLanguage();
1368 final boolean hasLanguage = !TextUtils.isEmpty(language);
1369 final String country = systemLocale.getCountry();
1370 final boolean hasCountry = !TextUtils.isEmpty(country);
1371 final String variant = systemLocale.getVariant();
1372 final boolean hasVariant = !TextUtils.isEmpty(variant);
1373 if (hasLanguage && hasCountry && hasVariant) {
1374 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
1375 } else {
1376 systemLocaleLanguageCountryVariant = null;
1377 }
1378 if (hasLanguage && hasCountry) {
1379 systemLocaleLanguageCountry = new Locale(language, country);
1380 } else {
1381 systemLocaleLanguageCountry = null;
1382 }
1383 if (hasLanguage) {
1384 systemLocaleLanguage = new Locale(language);
1385 } else {
1386 systemLocaleLanguage = null;
1387 }
1388 } else {
1389 systemLocaleLanguageCountryVariant = null;
1390 systemLocaleLanguageCountry = null;
1391 systemLocaleLanguage = null;
1392 }
1393
1394 final ArrayList<Locale> locales = new ArrayList<>();
1395 if (systemLocaleLanguageCountryVariant != null) {
1396 locales.add(systemLocaleLanguageCountryVariant);
1397 }
1398
1399 if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
1400 if (systemLocaleLanguageCountry != null) {
1401 // If the system language is English, and the region is also explicitly specified,
1402 // following fallback order will be applied.
1403 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
1404 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
1405 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
1406 // - en
1407 if (systemLocaleLanguageCountry != null) {
1408 locales.add(systemLocaleLanguageCountry);
1409 }
1410 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
1411 locales.add(LOCALE_EN_US);
1412 }
1413 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
1414 locales.add(LOCALE_EN_GB);
1415 }
1416 locales.add(Locale.ENGLISH);
1417 } else {
1418 // If the system language is English, but no region is specified, following
1419 // fallback order will be applied.
1420 // - en
1421 // - en_US
1422 // - en_GB
1423 locales.add(Locale.ENGLISH);
1424 locales.add(LOCALE_EN_US);
1425 locales.add(LOCALE_EN_GB);
1426 }
1427 } else {
1428 // If the system language is not English, the fallback order will be
1429 // - systemLocaleLanguageCountry [if non-null]
1430 // - systemLocaleLanguage [if non-null]
1431 // - en_US
1432 // - en_GB
1433 // - en
1434 if (systemLocaleLanguageCountry != null) {
1435 locales.add(systemLocaleLanguageCountry);
1436 }
1437 if (systemLocaleLanguage != null) {
1438 locales.add(systemLocaleLanguage);
1439 }
1440 locales.add(LOCALE_EN_US);
1441 locales.add(LOCALE_EN_GB);
1442 locales.add(Locale.ENGLISH);
1443 }
1444 return locales;
1445 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001446}