blob: 2dcee1a9668f584f74d99b84927139b57558617f [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
90 public static String getStackTrace() {
91 final StringBuilder sb = new StringBuilder();
92 try {
93 throw new RuntimeException();
94 } catch (RuntimeException e) {
95 final StackTraceElement[] frames = e.getStackTrace();
96 // Start at 1 because the first frame is here and we don't care about it
97 for (int j = 1; j < frames.length; ++j) {
98 sb.append(frames[j].toString() + "\n");
99 }
100 }
101 return sb.toString();
102 }
Satoshi Kataoka87c29142013-07-31 23:11:54 +0900103
104 public static String getApiCallStack() {
105 String apiCallStack = "";
106 try {
107 throw new RuntimeException();
108 } catch (RuntimeException e) {
109 final StackTraceElement[] frames = e.getStackTrace();
110 for (int j = 1; j < frames.length; ++j) {
111 final String tempCallStack = frames[j].toString();
112 if (TextUtils.isEmpty(apiCallStack)) {
113 // Overwrite apiCallStack if it's empty
114 apiCallStack = tempCallStack;
115 } else if (tempCallStack.indexOf("Transact(") < 0) {
116 // Overwrite apiCallStack if it's not a binder call
117 apiCallStack = tempCallStack;
118 } else {
119 break;
120 }
121 }
122 }
123 return apiCallStack;
124 }
Satoshi Kataoka0766eb02013-07-31 18:30:13 +0900125 // ----------------------------------------------------------------------
126
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900127 public static boolean isSystemIme(InputMethodInfo inputMethod) {
128 return (inputMethod.getServiceInfo().applicationInfo.flags
129 & ApplicationInfo.FLAG_SYSTEM) != 0;
130 }
131
Yohei Yukawa9c83ff42015-03-12 15:31:25 +0900132 public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900133 final Context context, final boolean checkDefaultAttribute,
134 @Nullable final Locale requiredLocale, final boolean checkCountry,
135 final String requiredSubtypeMode) {
136 if (!isSystemIme(imi)) {
137 return false;
138 }
139 if (checkDefaultAttribute && !imi.isDefault(context)) {
140 return false;
141 }
142 if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
143 return false;
144 }
145 return true;
146 }
147
148 @Nullable
Yohei Yukawadc489242014-09-14 12:01:59 +0900149 public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis,
150 final Context context) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900151 // At first, find the fallback locale from the IMEs that are declared as "default" in the
152 // current locale. Note that IME developers can declare an IME as "default" only for
153 // some particular locales but "not default" for other locales.
Yohei Yukawadc489242014-09-14 12:01:59 +0900154 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
155 for (int i = 0; i < imis.size(); ++i) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900156 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
157 true /* checkDefaultAttribute */, fallbackLocale,
158 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900159 return fallbackLocale;
160 }
161 }
162 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900163 // If no fallback locale is found in the above condition, find fallback locales regardless
164 // of the "default" attribute as a last resort.
165 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
166 for (int i = 0; i < imis.size(); ++i) {
167 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
168 false /* checkDefaultAttribute */, fallbackLocale,
169 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
170 return fallbackLocale;
171 }
172 }
173 }
174 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
Yohei Yukawadc489242014-09-14 12:01:59 +0900175 return null;
176 }
177
Yohei Yukawab21220e2014-11-01 21:04:30 +0900178 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi,
179 final Context context, final boolean checkDefaultAttribute) {
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900180 if (!isSystemIme(imi)) {
181 return false;
182 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900183 if (checkDefaultAttribute && !imi.isDefault(context)) {
184 return false;
185 }
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900186 if (!imi.isAuxiliaryIme()) {
187 return false;
188 }
189 final int subtypeCount = imi.getSubtypeCount();
190 for (int i = 0; i < subtypeCount; ++i) {
191 final InputMethodSubtype s = imi.getSubtypeAt(i);
192 if (s.overridesImplicitlyEnabledSubtype()) {
193 return true;
194 }
195 }
196 return false;
197 }
198
Yohei Yukawadc489242014-09-14 12:01:59 +0900199 public static Locale getSystemLocaleFromContext(final Context context) {
200 try {
201 return context.getResources().getConfiguration().locale;
202 } catch (Resources.NotFoundException ex) {
203 return null;
204 }
205 }
206
Yohei Yukawab21220e2014-11-01 21:04:30 +0900207 private static final class InputMethodListBuilder {
208 // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
209 // order can have non-trivial effect in the call sites.
210 @NonNull
211 private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
Yohei Yukawadc489242014-09-14 12:01:59 +0900212
Yohei Yukawab21220e2014-11-01 21:04:30 +0900213 public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis,
214 final Context context, final boolean checkDefaultAttribute,
215 @Nullable final Locale locale, final boolean checkCountry,
216 final String requiredSubtypeMode) {
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900217 for (int i = 0; i < imis.size(); ++i) {
218 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900219 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
220 checkCountry, requiredSubtypeMode)) {
221 mInputMethodSet.add(imi);
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900222 }
223 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900224 return this;
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900225 }
226
Yohei Yukawab21220e2014-11-01 21:04:30 +0900227 // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
228 // documented more clearly.
229 public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis,
230 final Context context) {
231 // If one or more auxiliary input methods are available, OK to stop populating the list.
232 for (final InputMethodInfo imi : mInputMethodSet) {
233 if (imi.isAuxiliaryIme()) {
234 return this;
235 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900236 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900237 boolean added = false;
Yohei Yukawadc489242014-09-14 12:01:59 +0900238 for (int i = 0; i < imis.size(); ++i) {
239 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900240 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
241 true /* checkDefaultAttribute */)) {
242 mInputMethodSet.add(imi);
243 added = true;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900244 }
245 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900246 if (added) {
247 return this;
248 }
249 for (int i = 0; i < imis.size(); ++i) {
250 final InputMethodInfo imi = imis.get(i);
251 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
252 false /* checkDefaultAttribute */)) {
253 mInputMethodSet.add(imi);
254 }
255 }
256 return this;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900257 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900258
Yohei Yukawab21220e2014-11-01 21:04:30 +0900259 public boolean isEmpty() {
260 return mInputMethodSet.isEmpty();
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900261 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900262
263 @NonNull
264 public ArrayList<InputMethodInfo> build() {
265 return new ArrayList<>(mInputMethodSet);
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900266 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900267 }
268
Yohei Yukawab21220e2014-11-01 21:04:30 +0900269 private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale(
270 final ArrayList<InputMethodInfo> imis, final Context context,
271 @Nullable final Locale fallbackLocale) {
272 // Before the system becomes ready, we pick up at least one keyboard in the following order.
273 // The first user (device owner) falls into this category.
274 // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
275 // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
276 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
277 // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
278 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
279
280 final InputMethodListBuilder builder = new InputMethodListBuilder();
281 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
282 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
283 if (!builder.isEmpty()) {
284 return builder;
Yohei Yukawadc489242014-09-14 12:01:59 +0900285 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900286 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
287 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
288 if (!builder.isEmpty()) {
289 return builder;
290 }
291 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
292 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
293 if (!builder.isEmpty()) {
294 return builder;
295 }
296 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
297 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
298 if (!builder.isEmpty()) {
299 return builder;
300 }
301 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
302 + " fallbackLocale=" + fallbackLocale);
303 return builder;
304 }
305
306 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
307 final ArrayList<InputMethodInfo> imis, final Context context,
308 @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
309 // Once the system becomes ready, we pick up at least one keyboard in the following order.
310 // Secondary users fall into this category in general.
311 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
312 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
313 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
314 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
315 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
316 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
317 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
318
319 final InputMethodListBuilder builder = new InputMethodListBuilder();
320 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
321 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
322 if (!builder.isEmpty()) {
323 return builder;
324 }
325 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
326 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
327 if (!builder.isEmpty()) {
328 return builder;
329 }
330 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
331 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
332 if (!builder.isEmpty()) {
333 return builder;
334 }
335 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
336 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
337 if (!builder.isEmpty()) {
338 return builder;
339 }
340 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
341 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
342 if (!builder.isEmpty()) {
343 return builder;
344 }
345 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
346 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
347 if (!builder.isEmpty()) {
348 return builder;
349 }
350 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
351 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
352 return builder;
353 }
354
355 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
356 final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) {
357 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
358 if (!isSystemReady) {
359 // When the system is not ready, the system locale is not stable and reliable. Hence
360 // we will pick up IMEs that support software keyboard based on the fallback locale.
361 // Also pick up suitable IMEs regardless of the software keyboard support.
362 // (e.g. Voice IMEs)
363 return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale)
364 .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
365 true /* checkCountry */, SUBTYPE_MODE_ANY)
366 .build();
367 }
368
369 // When the system is ready, we will primarily rely on the system locale, but also keep
370 // relying on the fallback locale as a last resort.
371 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
372 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
373 // subtype)
374 final Locale systemLocale = getSystemLocaleFromContext(context);
375 return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
376 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
377 true /* checkCountry */, SUBTYPE_MODE_ANY)
378 .fillAuxiliaryImes(imis, context)
379 .build();
Yohei Yukawadc489242014-09-14 12:01:59 +0900380 }
381
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900382 public static Locale constructLocaleFromString(String localeStr) {
383 if (TextUtils.isEmpty(localeStr)) {
384 return null;
385 }
386 // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
387 String[] localeParams = localeStr.split("_", 3);
388 // The length of localeStr is guaranteed to always return a 1 <= value <= 3
389 // because localeStr is not empty.
390 if (localeParams.length == 1) {
Yohei Yukawa92280cd2015-06-02 16:50:14 -0700391 if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
392 // Convert a locale whose language is "tl" to one whose language is "fil".
393 // For example, "tl_PH" will get converted to "fil_PH".
394 // Versions of Android earlier than Lollipop did not support three letter language
395 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
396 // On Lollipop and above, the current three letter version must be used.
397 localeParams[0] = "fil";
398 }
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900399 return new Locale(localeParams[0]);
400 } else if (localeParams.length == 2) {
401 return new Locale(localeParams[0], localeParams[1]);
402 } else if (localeParams.length == 3) {
403 return new Locale(localeParams[0], localeParams[1], localeParams[2]);
404 }
405 return null;
406 }
407
Yohei Yukawadc489242014-09-14 12:01:59 +0900408 public static boolean containsSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900409 @Nullable final Locale locale, final boolean checkCountry, final String mode) {
410 if (locale == null) {
411 return false;
412 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900413 final int N = imi.getSubtypeCount();
414 for (int i = 0; i < N; ++i) {
415 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900416 if (checkCountry) {
Yohei Yukawa92280cd2015-06-02 16:50:14 -0700417 final Locale subtypeLocale = subtype.getLocaleObject();
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900418 if (subtypeLocale == null ||
419 !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
420 !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900421 continue;
422 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900423 } else {
424 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
425 subtype.getLocale()));
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900426 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900427 continue;
428 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900429 }
430 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
431 mode.equalsIgnoreCase(subtype.getMode())) {
432 return true;
433 }
434 }
435 return false;
436 }
437
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900438 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700439 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900440 final int subtypeCount = imi.getSubtypeCount();
441 for (int i = 0; i < subtypeCount; ++i) {
442 subtypes.add(imi.getSubtypeAt(i));
443 }
444 return subtypes;
445 }
446
447 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
448 InputMethodInfo imi, String mode) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700449 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900450 final int subtypeCount = imi.getSubtypeCount();
451 for (int i = 0; i < subtypeCount; ++i) {
452 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
453 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
454 subtypes.add(subtype);
455 }
456 }
457 return subtypes;
458 }
459
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900460 public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900461 if (enabledImes == null || enabledImes.isEmpty()) {
462 return null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900463 }
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900464 // We'd prefer to fall back on a system IME, since that is safer.
465 int i = enabledImes.size();
466 int firstFoundSystemIme = -1;
467 while (i > 0) {
468 i--;
469 final InputMethodInfo imi = enabledImes.get(i);
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900470 if (imi.isAuxiliaryIme()) {
471 continue;
472 }
473 if (InputMethodUtils.isSystemIme(imi)
474 && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
475 SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900476 return imi;
477 }
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900478 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900479 firstFoundSystemIme = i;
480 }
481 }
482 return enabledImes.get(Math.max(firstFoundSystemIme, 0));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900483 }
484
485 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
486 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
487 }
488
489 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
490 if (imi != null) {
491 final int subtypeCount = imi.getSubtypeCount();
492 for (int i = 0; i < subtypeCount; ++i) {
493 InputMethodSubtype ims = imi.getSubtypeAt(i);
494 if (subtypeHashCode == ims.hashCode()) {
495 return i;
496 }
497 }
498 }
499 return NOT_A_SUBTYPE_ID;
500 }
501
Yohei Yukawae72d1c82015-02-20 20:55:21 +0900502 @VisibleForTesting
503 public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900504 Resources res, InputMethodInfo imi) {
505 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
506 final String systemLocale = res.getConfiguration().locale.toString();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700507 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100508 final String systemLanguage = res.getConfiguration().locale.getLanguage();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700509 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900510 final int N = subtypes.size();
511 for (int i = 0; i < N; ++i) {
512 // scan overriding implicitly enabled subtypes.
513 InputMethodSubtype subtype = subtypes.get(i);
514 if (subtype.overridesImplicitlyEnabledSubtype()) {
515 final String mode = subtype.getMode();
516 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
517 applicableModeAndSubtypesMap.put(mode, subtype);
518 }
519 }
520 }
521 if (applicableModeAndSubtypesMap.size() > 0) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700522 return new ArrayList<>(applicableModeAndSubtypesMap.values());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900523 }
524 for (int i = 0; i < N; ++i) {
525 final InputMethodSubtype subtype = subtypes.get(i);
526 final String locale = subtype.getLocale();
527 final String mode = subtype.getMode();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100528 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900529 // When system locale starts with subtype's locale, that subtype will be applicable
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100530 // for system locale. We need to make sure the languages are the same, to prevent
531 // locales like "fil" (Filipino) being matched by "fi" (Finnish).
532 //
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900533 // For instance, it's clearly applicable for cases like system locale = en_US and
534 // subtype = en, but it is not necessarily considered applicable for cases like system
535 // locale = en and subtype = en_US.
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100536 //
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900537 // We just call systemLocale.startsWith(locale) in this function because there is no
538 // need to find applicable subtypes aggressively unlike
539 // findLastResortApplicableSubtypeLocked.
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100540 //
541 // TODO: This check is broken. It won't take scripts into account and doesn't
542 // account for the mandatory conversions performed by Locale#toString.
543 if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900544 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
545 // If more applicable subtypes are contained, skip.
546 if (applicableSubtype != null) {
547 if (systemLocale.equals(applicableSubtype.getLocale())) continue;
548 if (!systemLocale.equals(locale)) continue;
549 }
550 applicableModeAndSubtypesMap.put(mode, subtype);
551 }
552 }
553 final InputMethodSubtype keyboardSubtype
554 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700555 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900556 applicableModeAndSubtypesMap.values());
557 if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
558 for (int i = 0; i < N; ++i) {
559 final InputMethodSubtype subtype = subtypes.get(i);
560 final String mode = subtype.getMode();
561 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
562 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
563 applicableSubtypes.add(subtype);
564 }
565 }
566 }
567 if (keyboardSubtype == null) {
568 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
569 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
570 if (lastResortKeyboardSubtype != null) {
571 applicableSubtypes.add(lastResortKeyboardSubtype);
572 }
573 }
574 return applicableSubtypes;
575 }
576
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900577 /**
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100578 * Returns the language component of a given locale string.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900579 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100580 */
Tadashi G. Takaoka77cbcb62014-07-12 16:08:20 +0900581 public static String getLanguageFromLocaleString(String locale) {
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100582 final int idx = locale.indexOf('_');
583 if (idx < 0) {
584 return locale;
585 } else {
586 return locale.substring(0, idx);
587 }
588 }
589
590 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900591 * If there are no selected subtypes, tries finding the most applicable one according to the
592 * given locale.
593 * @param subtypes this function will search the most applicable subtype in subtypes
594 * @param mode subtypes will be filtered by mode
595 * @param locale subtypes will be filtered by locale
596 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
597 * it will return the first subtype matched with mode
598 * @return the most applicable subtypeId
599 */
600 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
601 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
602 boolean canIgnoreLocaleAsLastResort) {
603 if (subtypes == null || subtypes.size() == 0) {
604 return null;
605 }
606 if (TextUtils.isEmpty(locale)) {
607 locale = res.getConfiguration().locale.toString();
608 }
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100609 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900610 boolean partialMatchFound = false;
611 InputMethodSubtype applicableSubtype = null;
612 InputMethodSubtype firstMatchedModeSubtype = null;
613 final int N = subtypes.size();
614 for (int i = 0; i < N; ++i) {
615 InputMethodSubtype subtype = subtypes.get(i);
616 final String subtypeLocale = subtype.getLocale();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100617 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900618 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
619 // and all subtypes with all modes can be candidates.
620 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
621 if (firstMatchedModeSubtype == null) {
622 firstMatchedModeSubtype = subtype;
623 }
624 if (locale.equals(subtypeLocale)) {
625 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
626 applicableSubtype = subtype;
627 break;
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100628 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900629 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
630 applicableSubtype = subtype;
631 partialMatchFound = true;
632 }
633 }
634 }
635
636 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
637 return firstMatchedModeSubtype;
638 }
639
640 // The first subtype applicable to the system locale will be defined as the most applicable
641 // subtype.
642 if (DEBUG) {
643 if (applicableSubtype != null) {
644 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
645 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
646 }
647 }
648 return applicableSubtype;
649 }
650
651 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
652 if (subtype == null) return true;
653 return !subtype.isAuxiliary();
654 }
655
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900656 public static void setNonSelectedSystemImesDisabledUntilUsed(
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700657 IPackageManager packageManager, List<InputMethodInfo> enabledImis,
658 int userId, String callingPackage) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900659 if (DEBUG) {
660 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
661 }
662 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
663 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
664 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
665 return;
666 }
667 // Only the current spell checker should be treated as an enabled one.
668 final SpellCheckerInfo currentSpellChecker =
669 TextServicesManager.getInstance().getCurrentSpellChecker();
670 for (final String packageName : systemImesDisabledUntilUsed) {
671 if (DEBUG) {
672 Slog.d(TAG, "check " + packageName);
673 }
674 boolean enabledIme = false;
675 for (int j = 0; j < enabledImis.size(); ++j) {
676 final InputMethodInfo imi = enabledImis.get(j);
677 if (packageName.equals(imi.getPackageName())) {
678 enabledIme = true;
679 break;
680 }
681 }
682 if (enabledIme) {
683 // enabled ime. skip
684 continue;
685 }
686 if (currentSpellChecker != null
687 && packageName.equals(currentSpellChecker.getPackageName())) {
688 // enabled spell checker. skip
689 if (DEBUG) {
690 Slog.d(TAG, packageName + " is the current spell checker. skip");
691 }
692 continue;
693 }
694 ApplicationInfo ai = null;
695 try {
696 ai = packageManager.getApplicationInfo(packageName,
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700697 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
698 } catch (RemoteException e) {
699 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
700 + " userId=" + userId, e);
701 continue;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900702 }
703 if (ai == null) {
704 // No app found for packageName
705 continue;
706 }
707 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
708 if (!isSystemPackage) {
709 continue;
710 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700711 setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900712 }
713 }
714
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700715 private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
716 int userId, String callingPackage) {
717 final int state;
718 try {
719 state = packageManager.getApplicationEnabledSetting(packageName, userId);
720 } catch (RemoteException e) {
721 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
722 + " userId=" + userId, e);
723 return;
724 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900725 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
726 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
727 if (DEBUG) {
728 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
729 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700730 try {
731 packageManager.setApplicationEnabledSetting(packageName,
732 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
733 0 /* newState */, userId, callingPackage);
734 } catch (RemoteException e) {
735 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
736 + " userId=" + userId + " callingPackage=" + callingPackage, e);
737 return;
738 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900739 } else {
740 if (DEBUG) {
741 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
742 }
743 }
744 }
745
Satoshi Kataokab2827262013-07-04 19:43:14 +0900746 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
747 InputMethodSubtype subtype) {
748 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
749 return subtype != null
750 ? TextUtils.concat(subtype.getDisplayName(context,
751 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
752 (TextUtils.isEmpty(imiLabel) ?
753 "" : " - " + imiLabel))
754 : imiLabel;
755 }
756
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900757 /**
Yohei Yukawae63b5fa2014-09-19 13:14:55 +0900758 * Returns true if a package name belongs to a UID.
759 *
760 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
761 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
762 * @param uid the UID to be validated.
763 * @param packageName the package name.
764 * @return {@code true} if the package name belongs to the UID.
765 */
766 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
767 final int uid, final String packageName) {
768 try {
769 appOpsManager.checkPackage(uid, packageName);
770 return true;
771 } catch (SecurityException e) {
772 return false;
773 }
774 }
775
776 /**
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900777 * Parses the setting stored input methods and subtypes string value.
778 *
779 * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
780 * @return Map from input method ID to set of input method subtypes IDs.
781 */
782 @VisibleForTesting
783 public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
784 @Nullable final String inputMethodsAndSubtypesString) {
785
786 final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<String, ArraySet<String>>();
787 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
788 return imeMap;
789 }
790
791 final SimpleStringSplitter typeSplitter =
792 new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
793 final SimpleStringSplitter subtypeSplitter =
794 new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
795
796 List<Pair<String, ArrayList<String>>> allImeSettings =
797 InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
798 typeSplitter,
799 subtypeSplitter);
800 for (Pair<String, ArrayList<String>> ime : allImeSettings) {
801 ArraySet<String> subtypes = new ArraySet<String>();
802 if (ime.second != null) {
803 subtypes.addAll(ime.second);
804 }
805 imeMap.put(ime.first, subtypes);
806 }
807 return imeMap;
808 }
809
810 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900811 * Utility class for putting and getting settings for InputMethod
812 * TODO: Move all putters and getters of settings to this class.
813 */
814 public static class InputMethodSettings {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900815 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700816 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900817
818 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700819 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900820
821 private final Resources mRes;
822 private final ContentResolver mResolver;
823 private final HashMap<String, InputMethodInfo> mMethodMap;
824 private final ArrayList<InputMethodInfo> mMethodList;
825
826 private String mEnabledInputMethodsStrCache;
827 private int mCurrentUserId;
Kenny Guy2a764942014-04-02 13:29:20 +0100828 private int[] mCurrentProfileIds = new int[0];
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900829
830 private static void buildEnabledInputMethodsSettingString(
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700831 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
832 builder.append(ime.first);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900833 // Inputmethod and subtypes are saved in the settings as follows:
834 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700835 for (String subtypeId: ime.second) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700836 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900837 }
838 }
839
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700840 public static String buildInputMethodsSettingString(
841 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
842 final StringBuilder b = new StringBuilder();
843 boolean needsSeparator = false;
844 for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
845 if (needsSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700846 b.append(INPUT_METHOD_SEPARATOR);
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700847 }
848 buildEnabledInputMethodsSettingString(b, ime);
849 needsSeparator = true;
850 }
851 return b.toString();
852 }
853
854 public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
855 String enabledInputMethodsStr,
856 TextUtils.SimpleStringSplitter inputMethodSplitter,
857 TextUtils.SimpleStringSplitter subtypeSplitter) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700858 ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700859 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
860 return imsList;
861 }
862 inputMethodSplitter.setString(enabledInputMethodsStr);
863 while (inputMethodSplitter.hasNext()) {
864 String nextImsStr = inputMethodSplitter.next();
865 subtypeSplitter.setString(nextImsStr);
866 if (subtypeSplitter.hasNext()) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700867 ArrayList<String> subtypeHashes = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700868 // The first element is ime id.
869 String imeId = subtypeSplitter.next();
870 while (subtypeSplitter.hasNext()) {
871 subtypeHashes.add(subtypeSplitter.next());
872 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700873 imsList.add(new Pair<>(imeId, subtypeHashes));
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700874 }
875 }
876 return imsList;
877 }
878
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900879 public InputMethodSettings(
880 Resources res, ContentResolver resolver,
881 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
882 int userId) {
883 setCurrentUserId(userId);
884 mRes = res;
885 mResolver = resolver;
886 mMethodMap = methodMap;
887 mMethodList = methodList;
888 }
889
890 public void setCurrentUserId(int userId) {
891 if (DEBUG) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900892 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900893 }
894 // IMMS settings are kept per user, so keep track of current user
895 mCurrentUserId = userId;
896 }
897
Kenny Guy2a764942014-04-02 13:29:20 +0100898 public void setCurrentProfileIds(int[] currentProfileIds) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800899 synchronized (this) {
Kenny Guy2a764942014-04-02 13:29:20 +0100900 mCurrentProfileIds = currentProfileIds;
Amith Yamasani734983f2014-03-04 16:48:05 -0800901 }
902 }
903
Kenny Guy2a764942014-04-02 13:29:20 +0100904 public boolean isCurrentProfile(int userId) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800905 synchronized (this) {
Kenny Guyf4824a02014-04-02 19:17:41 +0100906 if (userId == mCurrentUserId) return true;
Kenny Guy2a764942014-04-02 13:29:20 +0100907 for (int i = 0; i < mCurrentProfileIds.length; i++) {
908 if (userId == mCurrentProfileIds[i]) return true;
Amith Yamasani734983f2014-03-04 16:48:05 -0800909 }
910 return false;
911 }
912 }
913
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900914 public List<InputMethodInfo> getEnabledInputMethodListLocked() {
915 return createEnabledInputMethodListLocked(
916 getEnabledInputMethodsAndSubtypeListLocked());
917 }
918
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900919 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
920 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
921 List<InputMethodSubtype> enabledSubtypes =
922 getEnabledInputMethodSubtypeListLocked(imi);
923 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
924 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
925 context.getResources(), imi);
926 }
927 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
928 }
929
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +0900930 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900931 InputMethodInfo imi) {
932 List<Pair<String, ArrayList<String>>> imsList =
933 getEnabledInputMethodsAndSubtypeListLocked();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700934 ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900935 if (imi != null) {
936 for (Pair<String, ArrayList<String>> imsPair : imsList) {
937 InputMethodInfo info = mMethodMap.get(imsPair.first);
938 if (info != null && info.getId().equals(imi.getId())) {
939 final int subtypeCount = info.getSubtypeCount();
940 for (int i = 0; i < subtypeCount; ++i) {
941 InputMethodSubtype ims = info.getSubtypeAt(i);
942 for (String s: imsPair.second) {
943 if (String.valueOf(ims.hashCode()).equals(s)) {
944 enabledSubtypes.add(ims);
945 }
946 }
947 }
948 break;
949 }
950 }
951 }
952 return enabledSubtypes;
953 }
954
955 // At the initial boot, the settings for input methods are not set,
956 // so we need to enable IME in that case.
957 public void enableAllIMEsIfThereIsNoEnabledIME() {
958 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
959 StringBuilder sb = new StringBuilder();
960 final int N = mMethodList.size();
961 for (int i = 0; i < N; i++) {
962 InputMethodInfo imi = mMethodList.get(i);
963 Slog.i(TAG, "Adding: " + imi.getId());
964 if (i > 0) sb.append(':');
965 sb.append(imi.getId());
966 }
967 putEnabledInputMethodsStr(sb.toString());
968 }
969 }
970
971 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700972 return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
973 mInputMethodSplitter,
974 mSubtypeSplitter);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900975 }
976
977 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
978 if (reloadInputMethodStr) {
979 getEnabledInputMethodsStr();
980 }
981 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
982 // Add in the newly enabled input method.
983 putEnabledInputMethodsStr(id);
984 } else {
985 putEnabledInputMethodsStr(
Seigo Nonakace2c7842015-08-17 08:47:36 -0700986 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900987 }
988 }
989
990 /**
991 * Build and put a string of EnabledInputMethods with removing specified Id.
992 * @return the specified id was removed or not.
993 */
994 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
995 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
996 boolean isRemoved = false;
997 boolean needsAppendSeparator = false;
998 for (Pair<String, ArrayList<String>> ims: imsList) {
999 String curId = ims.first;
1000 if (curId.equals(id)) {
1001 // We are disabling this input method, and it is
1002 // currently enabled. Skip it to remove from the
1003 // new list.
1004 isRemoved = true;
1005 } else {
1006 if (needsAppendSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001007 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001008 } else {
1009 needsAppendSeparator = true;
1010 }
1011 buildEnabledInputMethodsSettingString(builder, ims);
1012 }
1013 }
1014 if (isRemoved) {
1015 // Update the setting with the new list of input methods.
1016 putEnabledInputMethodsStr(builder.toString());
1017 }
1018 return isRemoved;
1019 }
1020
1021 private List<InputMethodInfo> createEnabledInputMethodListLocked(
1022 List<Pair<String, ArrayList<String>>> imsList) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001023 final ArrayList<InputMethodInfo> res = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001024 for (Pair<String, ArrayList<String>> ims: imsList) {
1025 InputMethodInfo info = mMethodMap.get(ims.first);
1026 if (info != null) {
1027 res.add(info);
1028 }
1029 }
1030 return res;
1031 }
1032
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001033 private void putEnabledInputMethodsStr(String str) {
1034 Settings.Secure.putStringForUser(
1035 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId);
1036 mEnabledInputMethodsStrCache = str;
1037 if (DEBUG) {
1038 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1039 }
1040 }
1041
Dianne Hackbornfd7aded2013-01-22 17:10:23 -08001042 public String getEnabledInputMethodsStr() {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001043 mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser(
1044 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId);
1045 if (DEBUG) {
1046 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1047 + ", " + mCurrentUserId);
1048 }
1049 return mEnabledInputMethodsStrCache;
1050 }
1051
1052 private void saveSubtypeHistory(
1053 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1054 StringBuilder builder = new StringBuilder();
1055 boolean isImeAdded = false;
1056 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001057 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001058 newSubtypeId);
1059 isImeAdded = true;
1060 }
1061 for (Pair<String, String> ime: savedImes) {
1062 String imeId = ime.first;
1063 String subtypeId = ime.second;
1064 if (TextUtils.isEmpty(subtypeId)) {
1065 subtypeId = NOT_A_SUBTYPE_ID_STR;
1066 }
1067 if (isImeAdded) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001068 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001069 } else {
1070 isImeAdded = true;
1071 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001072 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001073 subtypeId);
1074 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001075 // Remove the last INPUT_METHOD_SEPARATOR
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001076 putSubtypeHistoryStr(builder.toString());
1077 }
1078
1079 private void addSubtypeToHistory(String imeId, String subtypeId) {
1080 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1081 for (Pair<String, String> ime: subtypeHistory) {
1082 if (ime.first.equals(imeId)) {
1083 if (DEBUG) {
1084 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1085 + ime.second);
1086 }
1087 // We should break here
1088 subtypeHistory.remove(ime);
1089 break;
1090 }
1091 }
1092 if (DEBUG) {
1093 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1094 }
1095 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1096 }
1097
1098 private void putSubtypeHistoryStr(String str) {
1099 if (DEBUG) {
1100 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1101 }
1102 Settings.Secure.putStringForUser(
1103 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId);
1104 }
1105
1106 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1107 // Gets the first one from the history
1108 return getLastSubtypeForInputMethodLockedInternal(null);
1109 }
1110
1111 public String getLastSubtypeForInputMethodLocked(String imeId) {
1112 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1113 if (ime != null) {
1114 return ime.second;
1115 } else {
1116 return null;
1117 }
1118 }
1119
1120 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1121 List<Pair<String, ArrayList<String>>> enabledImes =
1122 getEnabledInputMethodsAndSubtypeListLocked();
1123 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1124 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1125 final String imeInTheHistory = imeAndSubtype.first;
1126 // If imeId is empty, returns the first IME and subtype in the history
1127 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1128 final String subtypeInTheHistory = imeAndSubtype.second;
1129 final String subtypeHashCode =
1130 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1131 enabledImes, imeInTheHistory, subtypeInTheHistory);
1132 if (!TextUtils.isEmpty(subtypeHashCode)) {
1133 if (DEBUG) {
1134 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1135 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001136 return new Pair<>(imeInTheHistory, subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001137 }
1138 }
1139 }
1140 if (DEBUG) {
1141 Slog.d(TAG, "No enabled IME found in the history");
1142 }
1143 return null;
1144 }
1145
1146 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1147 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1148 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1149 if (enabledIme.first.equals(imeId)) {
1150 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1151 final InputMethodInfo imi = mMethodMap.get(imeId);
1152 if (explicitlyEnabledSubtypes.size() == 0) {
1153 // If there are no explicitly enabled subtypes, applicable subtypes are
1154 // enabled implicitly.
1155 // If IME is enabled and no subtypes are enabled, applicable subtypes
1156 // are enabled implicitly, so needs to treat them to be enabled.
1157 if (imi != null && imi.getSubtypeCount() > 0) {
1158 List<InputMethodSubtype> implicitlySelectedSubtypes =
1159 getImplicitlyApplicableSubtypesLocked(mRes, imi);
1160 if (implicitlySelectedSubtypes != null) {
1161 final int N = implicitlySelectedSubtypes.size();
1162 for (int i = 0; i < N; ++i) {
1163 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1164 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1165 return subtypeHashCode;
1166 }
1167 }
1168 }
1169 }
1170 } else {
1171 for (String s: explicitlyEnabledSubtypes) {
1172 if (s.equals(subtypeHashCode)) {
1173 // If both imeId and subtypeId are enabled, return subtypeId.
1174 try {
1175 final int hashCode = Integer.valueOf(subtypeHashCode);
1176 // Check whether the subtype id is valid or not
1177 if (isValidSubtypeId(imi, hashCode)) {
1178 return s;
1179 } else {
1180 return NOT_A_SUBTYPE_ID_STR;
1181 }
1182 } catch (NumberFormatException e) {
1183 return NOT_A_SUBTYPE_ID_STR;
1184 }
1185 }
1186 }
1187 }
1188 // If imeId was enabled but subtypeId was disabled.
1189 return NOT_A_SUBTYPE_ID_STR;
1190 }
1191 }
1192 // If both imeId and subtypeId are disabled, return null
1193 return null;
1194 }
1195
1196 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001197 ArrayList<Pair<String, String>> imsList = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001198 final String subtypeHistoryStr = getSubtypeHistoryStr();
1199 if (TextUtils.isEmpty(subtypeHistoryStr)) {
1200 return imsList;
1201 }
1202 mInputMethodSplitter.setString(subtypeHistoryStr);
1203 while (mInputMethodSplitter.hasNext()) {
1204 String nextImsStr = mInputMethodSplitter.next();
1205 mSubtypeSplitter.setString(nextImsStr);
1206 if (mSubtypeSplitter.hasNext()) {
1207 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1208 // The first element is ime id.
1209 String imeId = mSubtypeSplitter.next();
1210 while (mSubtypeSplitter.hasNext()) {
1211 subtypeId = mSubtypeSplitter.next();
1212 break;
1213 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001214 imsList.add(new Pair<>(imeId, subtypeId));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001215 }
1216 }
1217 return imsList;
1218 }
1219
1220 private String getSubtypeHistoryStr() {
1221 if (DEBUG) {
1222 Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser(
1223 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId));
1224 }
1225 return Settings.Secure.getStringForUser(
1226 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId);
1227 }
1228
1229 public void putSelectedInputMethod(String imeId) {
1230 if (DEBUG) {
1231 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1232 + mCurrentUserId);
1233 }
1234 Settings.Secure.putStringForUser(
1235 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId);
1236 }
1237
1238 public void putSelectedSubtype(int subtypeId) {
1239 if (DEBUG) {
1240 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1241 + mCurrentUserId);
1242 }
1243 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
1244 subtypeId, mCurrentUserId);
1245 }
1246
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001247 public String getSelectedInputMethod() {
1248 if (DEBUG) {
1249 Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser(
1250 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId)
1251 + ", " + mCurrentUserId);
1252 }
1253 return Settings.Secure.getStringForUser(
1254 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId);
1255 }
1256
1257 public boolean isSubtypeSelected() {
1258 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1259 }
1260
1261 private int getSelectedInputMethodSubtypeHashCode() {
1262 try {
1263 return Settings.Secure.getIntForUser(
1264 mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId);
1265 } catch (SettingNotFoundException e) {
1266 return NOT_A_SUBTYPE_ID;
1267 }
1268 }
1269
Michael Wright7b5a96b2014-08-09 19:28:42 -07001270 public boolean isShowImeWithHardKeyboardEnabled() {
1271 return Settings.Secure.getIntForUser(mResolver,
1272 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0, mCurrentUserId) == 1;
1273 }
1274
1275 public void setShowImeWithHardKeyboard(boolean show) {
1276 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
1277 show ? 1 : 0, mCurrentUserId);
1278 }
1279
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001280 public int getCurrentUserId() {
1281 return mCurrentUserId;
1282 }
1283
1284 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
1285 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1286 if (imi == null) {
1287 return NOT_A_SUBTYPE_ID;
1288 }
1289 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1290 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1291 }
1292
1293 public void saveCurrentInputMethodAndSubtypeToHistory(
1294 String curMethodId, InputMethodSubtype currentSubtype) {
1295 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1296 if (currentSubtype != null) {
1297 subtypeId = String.valueOf(currentSubtype.hashCode());
1298 }
1299 if (canAddToLastInputMethod(currentSubtype)) {
1300 addSubtypeToHistory(curMethodId, subtypeId);
1301 }
1302 }
Satoshi Kataokad787f692013-10-26 04:44:21 +09001303
1304 public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1305 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1306 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001307 new HashMap<>();
Satoshi Kataokad787f692013-10-26 04:44:21 +09001308 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1309 enabledInputMethodAndSubtypes.put(
1310 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1311 }
1312 return enabledInputMethodAndSubtypes;
1313 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001314 }
Yohei Yukawa174843a2015-06-26 18:02:54 -07001315
1316 // For spell checker service manager.
1317 // TODO: Should we have TextServicesUtils.java?
1318 private static final Locale LOCALE_EN_US = new Locale("en", "US");
1319 private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
1320
1321 /**
1322 * Returns a list of {@link Locale} in the order of appropriateness for the default spell
1323 * checker service.
1324 *
1325 * <p>If the system language is English, and the region is also explicitly specified in the
1326 * system locale, the following fallback order will be applied.</p>
1327 * <ul>
1328 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1329 * <li>(system-locale-language, system-locale-region)</li>
1330 * <li>("en", "US")</li>
1331 * <li>("en", "GB")</li>
1332 * <li>("en")</li>
1333 * </ul>
1334 *
1335 * <p>If the system language is English, but no region is specified in the system locale,
1336 * the following fallback order will be applied.</p>
1337 * <ul>
1338 * <li>("en")</li>
1339 * <li>("en", "US")</li>
1340 * <li>("en", "GB")</li>
1341 * </ul>
1342 *
1343 * <p>If the system language is not English, the following fallback order will be applied.</p>
1344 * <ul>
1345 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1346 * <li>(system-locale-language, system-locale-region) (if exists)</li>
1347 * <li>(system-locale-language) (if exists)</li>
1348 * <li>("en", "US")</li>
1349 * <li>("en", "GB")</li>
1350 * <li>("en")</li>
1351 * </ul>
1352 *
1353 * @param systemLocale the current system locale to be taken into consideration.
1354 * @return a list of {@link Locale}. The first one is considered to be most appropriate.
1355 */
1356 @VisibleForTesting
1357 public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
1358 @Nullable final Locale systemLocale) {
1359 final Locale systemLocaleLanguageCountryVariant;
1360 final Locale systemLocaleLanguageCountry;
1361 final Locale systemLocaleLanguage;
1362 if (systemLocale != null) {
1363 final String language = systemLocale.getLanguage();
1364 final boolean hasLanguage = !TextUtils.isEmpty(language);
1365 final String country = systemLocale.getCountry();
1366 final boolean hasCountry = !TextUtils.isEmpty(country);
1367 final String variant = systemLocale.getVariant();
1368 final boolean hasVariant = !TextUtils.isEmpty(variant);
1369 if (hasLanguage && hasCountry && hasVariant) {
1370 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
1371 } else {
1372 systemLocaleLanguageCountryVariant = null;
1373 }
1374 if (hasLanguage && hasCountry) {
1375 systemLocaleLanguageCountry = new Locale(language, country);
1376 } else {
1377 systemLocaleLanguageCountry = null;
1378 }
1379 if (hasLanguage) {
1380 systemLocaleLanguage = new Locale(language);
1381 } else {
1382 systemLocaleLanguage = null;
1383 }
1384 } else {
1385 systemLocaleLanguageCountryVariant = null;
1386 systemLocaleLanguageCountry = null;
1387 systemLocaleLanguage = null;
1388 }
1389
1390 final ArrayList<Locale> locales = new ArrayList<>();
1391 if (systemLocaleLanguageCountryVariant != null) {
1392 locales.add(systemLocaleLanguageCountryVariant);
1393 }
1394
1395 if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
1396 if (systemLocaleLanguageCountry != null) {
1397 // If the system language is English, and the region is also explicitly specified,
1398 // following fallback order will be applied.
1399 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
1400 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
1401 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
1402 // - en
1403 if (systemLocaleLanguageCountry != null) {
1404 locales.add(systemLocaleLanguageCountry);
1405 }
1406 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
1407 locales.add(LOCALE_EN_US);
1408 }
1409 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
1410 locales.add(LOCALE_EN_GB);
1411 }
1412 locales.add(Locale.ENGLISH);
1413 } else {
1414 // If the system language is English, but no region is specified, following
1415 // fallback order will be applied.
1416 // - en
1417 // - en_US
1418 // - en_GB
1419 locales.add(Locale.ENGLISH);
1420 locales.add(LOCALE_EN_US);
1421 locales.add(LOCALE_EN_GB);
1422 }
1423 } else {
1424 // If the system language is not English, the fallback order will be
1425 // - systemLocaleLanguageCountry [if non-null]
1426 // - systemLocaleLanguage [if non-null]
1427 // - en_US
1428 // - en_GB
1429 // - en
1430 if (systemLocaleLanguageCountry != null) {
1431 locales.add(systemLocaleLanguageCountry);
1432 }
1433 if (systemLocaleLanguage != null) {
1434 locales.add(systemLocaleLanguage);
1435 }
1436 locales.add(LOCALE_EN_US);
1437 locales.add(LOCALE_EN_GB);
1438 locales.add(Locale.ENGLISH);
1439 }
1440 return locales;
1441 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001442}