blob: 3e231d0aaa8b7a1e08eca0ac08c6515086ccdf35 [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 Yukawa68645a62016-02-17 07:54:20 -080021import android.annotation.UserIdInt;
Yohei Yukawae63b5fa2014-09-19 13:14:55 +090022import android.app.AppOpsManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090023import android.content.ContentResolver;
24import android.content.Context;
25import android.content.pm.ApplicationInfo;
Yohei Yukawa094c71f2015-06-20 00:41:31 -070026import android.content.pm.IPackageManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090027import android.content.pm.PackageManager;
28import android.content.res.Resources;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070029import android.os.LocaleList;
Yohei Yukawa094c71f2015-06-20 00:41:31 -070030import android.os.RemoteException;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090031import android.provider.Settings;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090032import android.text.TextUtils;
Seigo Nonaka2028dda2015-07-06 17:41:24 +090033import android.text.TextUtils.SimpleStringSplitter;
34import android.util.ArrayMap;
35import android.util.ArraySet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090036import android.util.Pair;
Yohei Yukawa68645a62016-02-17 07:54:20 -080037import android.util.Printer;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090038import android.util.Slog;
39import android.view.inputmethod.InputMethodInfo;
40import android.view.inputmethod.InputMethodSubtype;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +090041import android.view.textservice.SpellCheckerInfo;
42import android.view.textservice.TextServicesManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090043
Yohei Yukawaccb024a2016-06-13 22:16:52 -070044import com.android.internal.annotations.GuardedBy;
Yohei Yukawae72d1c82015-02-20 20:55:21 +090045import com.android.internal.annotations.VisibleForTesting;
46
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090047import java.util.ArrayList;
Yohei Yukawab21220e2014-11-01 21:04:30 +090048import java.util.Arrays;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090049import java.util.HashMap;
Yohei Yukawab21220e2014-11-01 21:04:30 +090050import java.util.LinkedHashSet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090051import java.util.List;
52import java.util.Locale;
53
54/**
55 * InputMethodManagerUtils contains some static methods that provides IME informations.
56 * This methods are supposed to be used in both the framework and the Settings application.
57 */
58public class InputMethodUtils {
59 public static final boolean DEBUG = false;
60 public static final int NOT_A_SUBTYPE_ID = -1;
Yohei Yukawa68c860b2014-09-13 22:03:37 +090061 public static final String SUBTYPE_MODE_ANY = null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090062 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
63 public static final String SUBTYPE_MODE_VOICE = "voice";
64 private static final String TAG = "InputMethodUtils";
65 private static final Locale ENGLISH_LOCALE = new Locale("en");
66 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
67 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
68 "EnabledWhenDefaultIsNotAsciiCapable";
69 private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
Seigo Nonakace2c7842015-08-17 08:47:36 -070070
71 // The string for enabled input method is saved as follows:
72 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
Seigo Nonaka2028dda2015-07-06 17:41:24 +090073 private static final char INPUT_METHOD_SEPARATOR = ':';
74 private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
Yohei Yukawadc489242014-09-14 12:01:59 +090075 /**
76 * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
77 * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
78 * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
79 * doesn't automatically match {@code Locale("en", "IN")}.
80 */
81 private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
82 Locale.ENGLISH, // "en"
83 Locale.US, // "en_US"
84 Locale.UK, // "en_GB"
85 };
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090086
Yohei Yukawaccb024a2016-06-13 22:16:52 -070087 // A temporary workaround for the performance concerns in
88 // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
89 // TODO: Optimize all the critical paths including this one.
90 private static final Object sCacheLock = new Object();
91 @GuardedBy("sCacheLock")
92 private static LocaleList sCachedSystemLocales;
93 @GuardedBy("sCacheLock")
94 private static InputMethodInfo sCachedInputMethodInfo;
95 @GuardedBy("sCacheLock")
96 private static ArrayList<InputMethodSubtype> sCachedResult;
97
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090098 private InputMethodUtils() {
99 // This utility class is not publicly instantiable.
100 }
101
Satoshi Kataoka0766eb02013-07-31 18:30:13 +0900102 // ----------------------------------------------------------------------
103 // Utilities for debug
Satoshi Kataoka87c29142013-07-31 23:11:54 +0900104 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 getMinimumKeyboardSetWithSystemLocale(
270 final ArrayList<InputMethodInfo> imis, final Context context,
271 @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
272 // Once the system becomes ready, we pick up at least one keyboard in the following order.
273 // Secondary users fall into this category in general.
274 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
275 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
276 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
277 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
278 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
279 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
280 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
281
282 final InputMethodListBuilder builder = new InputMethodListBuilder();
283 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
284 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
285 if (!builder.isEmpty()) {
286 return builder;
287 }
288 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
289 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
290 if (!builder.isEmpty()) {
291 return builder;
292 }
293 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
294 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
295 if (!builder.isEmpty()) {
296 return builder;
297 }
298 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
299 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
300 if (!builder.isEmpty()) {
301 return builder;
302 }
303 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
304 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
305 if (!builder.isEmpty()) {
306 return builder;
307 }
308 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
309 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
310 if (!builder.isEmpty()) {
311 return builder;
312 }
313 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
314 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
315 return builder;
316 }
317
318 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
Yohei Yukawaaf5cee82017-01-23 16:17:11 -0800319 final ArrayList<InputMethodInfo> imis) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900320 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
Yohei Yukawaaf5cee82017-01-23 16:17:11 -0800321 // We will primarily rely on the system locale, but also keep relying on the fallback locale
322 // as a last resort.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900323 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
324 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
325 // subtype)
326 final Locale systemLocale = getSystemLocaleFromContext(context);
327 return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
328 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
329 true /* checkCountry */, SUBTYPE_MODE_ANY)
330 .fillAuxiliaryImes(imis, context)
331 .build();
Yohei Yukawadc489242014-09-14 12:01:59 +0900332 }
333
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900334 public static Locale constructLocaleFromString(String localeStr) {
335 if (TextUtils.isEmpty(localeStr)) {
336 return null;
337 }
338 // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
339 String[] localeParams = localeStr.split("_", 3);
Yohei Yukawaed65bc02015-12-02 18:22:41 -0800340 if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
341 // Convert a locale whose language is "tl" to one whose language is "fil".
342 // For example, "tl_PH" will get converted to "fil_PH".
343 // Versions of Android earlier than Lollipop did not support three letter language
344 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
345 // On Lollipop and above, the current three letter version must be used.
346 localeParams[0] = "fil";
347 }
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900348 // The length of localeStr is guaranteed to always return a 1 <= value <= 3
349 // because localeStr is not empty.
350 if (localeParams.length == 1) {
351 return new Locale(localeParams[0]);
352 } else if (localeParams.length == 2) {
353 return new Locale(localeParams[0], localeParams[1]);
354 } else if (localeParams.length == 3) {
355 return new Locale(localeParams[0], localeParams[1], localeParams[2]);
356 }
357 return null;
358 }
359
Yohei Yukawadc489242014-09-14 12:01:59 +0900360 public static boolean containsSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900361 @Nullable final Locale locale, final boolean checkCountry, final String mode) {
362 if (locale == null) {
363 return false;
364 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900365 final int N = imi.getSubtypeCount();
366 for (int i = 0; i < N; ++i) {
367 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900368 if (checkCountry) {
Yohei Yukawa92280cd2015-06-02 16:50:14 -0700369 final Locale subtypeLocale = subtype.getLocaleObject();
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900370 if (subtypeLocale == null ||
371 !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
372 !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900373 continue;
374 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900375 } else {
376 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
377 subtype.getLocale()));
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900378 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900379 continue;
380 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900381 }
382 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
383 mode.equalsIgnoreCase(subtype.getMode())) {
384 return true;
385 }
386 }
387 return false;
388 }
389
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900390 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700391 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900392 final int subtypeCount = imi.getSubtypeCount();
393 for (int i = 0; i < subtypeCount; ++i) {
394 subtypes.add(imi.getSubtypeAt(i));
395 }
396 return subtypes;
397 }
398
399 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
400 InputMethodInfo imi, String mode) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700401 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900402 final int subtypeCount = imi.getSubtypeCount();
403 for (int i = 0; i < subtypeCount; ++i) {
404 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
405 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
406 subtypes.add(subtype);
407 }
408 }
409 return subtypes;
410 }
411
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900412 public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900413 if (enabledImes == null || enabledImes.isEmpty()) {
414 return null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900415 }
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900416 // We'd prefer to fall back on a system IME, since that is safer.
417 int i = enabledImes.size();
418 int firstFoundSystemIme = -1;
419 while (i > 0) {
420 i--;
421 final InputMethodInfo imi = enabledImes.get(i);
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900422 if (imi.isAuxiliaryIme()) {
423 continue;
424 }
425 if (InputMethodUtils.isSystemIme(imi)
426 && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
427 SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900428 return imi;
429 }
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900430 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900431 firstFoundSystemIme = i;
432 }
433 }
434 return enabledImes.get(Math.max(firstFoundSystemIme, 0));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900435 }
436
437 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
438 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
439 }
440
441 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
442 if (imi != null) {
443 final int subtypeCount = imi.getSubtypeCount();
444 for (int i = 0; i < subtypeCount; ++i) {
445 InputMethodSubtype ims = imi.getSubtypeAt(i);
446 if (subtypeHashCode == ims.hashCode()) {
447 return i;
448 }
449 }
450 }
451 return NOT_A_SUBTYPE_ID;
452 }
453
Yohei Yukawae985c242016-02-24 18:27:04 -0800454 private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
455 new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
456 @Override
457 public Locale get(InputMethodSubtype source) {
458 return source != null ? source.getLocaleObject() : null;
459 }
460 };
461
Yohei Yukawae72d1c82015-02-20 20:55:21 +0900462 @VisibleForTesting
463 public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900464 Resources res, InputMethodInfo imi) {
Yohei Yukawaccb024a2016-06-13 22:16:52 -0700465 final LocaleList systemLocales = res.getConfiguration().getLocales();
466
467 synchronized (sCacheLock) {
468 // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
469 // it does not check if subtypes are also identical.
470 if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
471 return new ArrayList<>(sCachedResult);
472 }
473 }
474
475 // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
476 // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
477 // LocaleList rather than Resource.
478 final ArrayList<InputMethodSubtype> result =
479 getImplicitlyApplicableSubtypesLockedImpl(res, imi);
480 synchronized (sCacheLock) {
481 // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
482 sCachedSystemLocales = systemLocales;
483 sCachedInputMethodInfo = imi;
484 sCachedResult = new ArrayList<>(result);
485 }
486 return result;
487 }
488
489 private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
490 Resources res, InputMethodInfo imi) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900491 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
Yohei Yukawae985c242016-02-24 18:27:04 -0800492 final LocaleList systemLocales = res.getConfiguration().getLocales();
493 final String systemLocale = systemLocales.get(0).toString();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700494 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800495 final int numSubtypes = subtypes.size();
496
497 // Handle overridesImplicitlyEnabledSubtype mechanism.
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700498 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800499 for (int i = 0; i < numSubtypes; ++i) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900500 // scan overriding implicitly enabled subtypes.
Yohei Yukawae985c242016-02-24 18:27:04 -0800501 final InputMethodSubtype subtype = subtypes.get(i);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900502 if (subtype.overridesImplicitlyEnabledSubtype()) {
503 final String mode = subtype.getMode();
504 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
505 applicableModeAndSubtypesMap.put(mode, subtype);
506 }
507 }
508 }
509 if (applicableModeAndSubtypesMap.size() > 0) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700510 return new ArrayList<>(applicableModeAndSubtypesMap.values());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900511 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800512
Yohei Yukawa238faad2016-03-11 10:39:51 -0800513 final HashMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
514 new HashMap<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800515 final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
Yohei Yukawa238faad2016-03-11 10:39:51 -0800516
Yohei Yukawae985c242016-02-24 18:27:04 -0800517 for (int i = 0; i < numSubtypes; ++i) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900518 final InputMethodSubtype subtype = subtypes.get(i);
Yohei Yukawa238faad2016-03-11 10:39:51 -0800519 final String mode = subtype.getMode();
520 if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
Yohei Yukawae985c242016-02-24 18:27:04 -0800521 keyboardSubtypes.add(subtype);
522 } else {
Yohei Yukawa238faad2016-03-11 10:39:51 -0800523 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
524 nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900525 }
Yohei Yukawa238faad2016-03-11 10:39:51 -0800526 nonKeyboardSubtypesMap.get(mode).add(subtype);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900527 }
528 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800529
530 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
531 LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
532 applicableSubtypes);
533
Yohei Yukawa42275bc2016-03-03 00:34:27 -0800534 if (!applicableSubtypes.isEmpty()) {
535 boolean hasAsciiCapableKeyboard = false;
536 final int numApplicationSubtypes = applicableSubtypes.size();
537 for (int i = 0; i < numApplicationSubtypes; ++i) {
538 final InputMethodSubtype subtype = applicableSubtypes.get(i);
539 if (subtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
540 hasAsciiCapableKeyboard = true;
541 break;
542 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800543 }
Yohei Yukawa42275bc2016-03-03 00:34:27 -0800544 if (!hasAsciiCapableKeyboard) {
545 final int numKeyboardSubtypes = keyboardSubtypes.size();
546 for (int i = 0; i < numKeyboardSubtypes; ++i) {
547 final InputMethodSubtype subtype = keyboardSubtypes.get(i);
548 final String mode = subtype.getMode();
549 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
550 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
551 applicableSubtypes.add(subtype);
552 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900553 }
554 }
555 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800556
557 if (applicableSubtypes.isEmpty()) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900558 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
559 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
560 if (lastResortKeyboardSubtype != null) {
561 applicableSubtypes.add(lastResortKeyboardSubtype);
562 }
563 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800564
Yohei Yukawa238faad2016-03-11 10:39:51 -0800565 // For each non-keyboard mode, extract subtypes with system locales.
566 for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
567 LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
568 applicableSubtypes);
569 }
570
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900571 return applicableSubtypes;
572 }
573
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900574 /**
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100575 * Returns the language component of a given locale string.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900576 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100577 */
Tadashi G. Takaoka77cbcb62014-07-12 16:08:20 +0900578 public static String getLanguageFromLocaleString(String locale) {
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100579 final int idx = locale.indexOf('_');
580 if (idx < 0) {
581 return locale;
582 } else {
583 return locale.substring(0, idx);
584 }
585 }
586
587 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900588 * If there are no selected subtypes, tries finding the most applicable one according to the
589 * given locale.
590 * @param subtypes this function will search the most applicable subtype in subtypes
591 * @param mode subtypes will be filtered by mode
592 * @param locale subtypes will be filtered by locale
593 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
594 * it will return the first subtype matched with mode
595 * @return the most applicable subtypeId
596 */
597 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
598 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
599 boolean canIgnoreLocaleAsLastResort) {
600 if (subtypes == null || subtypes.size() == 0) {
601 return null;
602 }
603 if (TextUtils.isEmpty(locale)) {
604 locale = res.getConfiguration().locale.toString();
605 }
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100606 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900607 boolean partialMatchFound = false;
608 InputMethodSubtype applicableSubtype = null;
609 InputMethodSubtype firstMatchedModeSubtype = null;
610 final int N = subtypes.size();
611 for (int i = 0; i < N; ++i) {
612 InputMethodSubtype subtype = subtypes.get(i);
613 final String subtypeLocale = subtype.getLocale();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100614 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900615 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
616 // and all subtypes with all modes can be candidates.
617 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
618 if (firstMatchedModeSubtype == null) {
619 firstMatchedModeSubtype = subtype;
620 }
621 if (locale.equals(subtypeLocale)) {
622 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
623 applicableSubtype = subtype;
624 break;
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100625 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900626 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
627 applicableSubtype = subtype;
628 partialMatchFound = true;
629 }
630 }
631 }
632
633 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
634 return firstMatchedModeSubtype;
635 }
636
637 // The first subtype applicable to the system locale will be defined as the most applicable
638 // subtype.
639 if (DEBUG) {
640 if (applicableSubtype != null) {
641 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
642 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
643 }
644 }
645 return applicableSubtype;
646 }
647
648 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
649 if (subtype == null) return true;
650 return !subtype.isAuxiliary();
651 }
652
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900653 public static void setNonSelectedSystemImesDisabledUntilUsed(
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700654 IPackageManager packageManager, List<InputMethodInfo> enabledImis,
655 int userId, String callingPackage) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900656 if (DEBUG) {
657 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
658 }
659 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
660 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
661 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
662 return;
663 }
664 // Only the current spell checker should be treated as an enabled one.
665 final SpellCheckerInfo currentSpellChecker =
666 TextServicesManager.getInstance().getCurrentSpellChecker();
667 for (final String packageName : systemImesDisabledUntilUsed) {
668 if (DEBUG) {
669 Slog.d(TAG, "check " + packageName);
670 }
671 boolean enabledIme = false;
672 for (int j = 0; j < enabledImis.size(); ++j) {
673 final InputMethodInfo imi = enabledImis.get(j);
674 if (packageName.equals(imi.getPackageName())) {
675 enabledIme = true;
676 break;
677 }
678 }
679 if (enabledIme) {
680 // enabled ime. skip
681 continue;
682 }
683 if (currentSpellChecker != null
684 && packageName.equals(currentSpellChecker.getPackageName())) {
685 // enabled spell checker. skip
686 if (DEBUG) {
687 Slog.d(TAG, packageName + " is the current spell checker. skip");
688 }
689 continue;
690 }
691 ApplicationInfo ai = null;
692 try {
693 ai = packageManager.getApplicationInfo(packageName,
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700694 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
695 } catch (RemoteException e) {
696 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
697 + " userId=" + userId, e);
698 continue;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900699 }
700 if (ai == null) {
701 // No app found for packageName
702 continue;
703 }
704 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
705 if (!isSystemPackage) {
706 continue;
707 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700708 setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900709 }
710 }
711
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700712 private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
713 int userId, String callingPackage) {
714 final int state;
715 try {
716 state = packageManager.getApplicationEnabledSetting(packageName, userId);
717 } catch (RemoteException e) {
718 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
719 + " userId=" + userId, e);
720 return;
721 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900722 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
723 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
724 if (DEBUG) {
725 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
726 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700727 try {
728 packageManager.setApplicationEnabledSetting(packageName,
729 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
730 0 /* newState */, userId, callingPackage);
731 } catch (RemoteException e) {
732 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
733 + " userId=" + userId + " callingPackage=" + callingPackage, e);
734 return;
735 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900736 } else {
737 if (DEBUG) {
738 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
739 }
740 }
741 }
742
Satoshi Kataokab2827262013-07-04 19:43:14 +0900743 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
744 InputMethodSubtype subtype) {
745 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
746 return subtype != null
747 ? TextUtils.concat(subtype.getDisplayName(context,
748 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
749 (TextUtils.isEmpty(imiLabel) ?
750 "" : " - " + imiLabel))
751 : imiLabel;
752 }
753
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900754 /**
Yohei Yukawae63b5fa2014-09-19 13:14:55 +0900755 * Returns true if a package name belongs to a UID.
756 *
757 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
758 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
759 * @param uid the UID to be validated.
760 * @param packageName the package name.
761 * @return {@code true} if the package name belongs to the UID.
762 */
763 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
764 final int uid, final String packageName) {
765 try {
766 appOpsManager.checkPackage(uid, packageName);
767 return true;
768 } catch (SecurityException e) {
769 return false;
770 }
771 }
772
773 /**
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900774 * Parses the setting stored input methods and subtypes string value.
775 *
776 * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
777 * @return Map from input method ID to set of input method subtypes IDs.
778 */
779 @VisibleForTesting
780 public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
781 @Nullable final String inputMethodsAndSubtypesString) {
782
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800783 final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900784 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
785 return imeMap;
786 }
787
788 final SimpleStringSplitter typeSplitter =
789 new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
790 final SimpleStringSplitter subtypeSplitter =
791 new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
792
793 List<Pair<String, ArrayList<String>>> allImeSettings =
794 InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
795 typeSplitter,
796 subtypeSplitter);
797 for (Pair<String, ArrayList<String>> ime : allImeSettings) {
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800798 ArraySet<String> subtypes = new ArraySet<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900799 if (ime.second != null) {
800 subtypes.addAll(ime.second);
801 }
802 imeMap.put(ime.first, subtypes);
803 }
804 return imeMap;
805 }
806
Seigo Nonaka2a099bc2015-08-14 19:29:45 -0700807 @NonNull
808 public static String buildInputMethodsAndSubtypesString(
809 @NonNull final ArrayMap<String, ArraySet<String>> map) {
810 // we want to use the canonical InputMethodSettings implementation,
811 // so we convert data structures first.
812 List<Pair<String, ArrayList<String>>> imeMap = new ArrayList<>(4);
813 for (ArrayMap.Entry<String, ArraySet<String>> entry : map.entrySet()) {
814 final String imeName = entry.getKey();
815 final ArraySet<String> subtypeSet = entry.getValue();
816 final ArrayList<String> subtypes = new ArrayList<>(2);
817 if (subtypeSet != null) {
818 subtypes.addAll(subtypeSet);
819 }
820 imeMap.add(new Pair<>(imeName, subtypes));
821 }
822 return InputMethodSettings.buildInputMethodsSettingString(imeMap);
823 }
824
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900825 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900826 * Utility class for putting and getting settings for InputMethod
827 * TODO: Move all putters and getters of settings to this class.
828 */
829 public static class InputMethodSettings {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900830 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700831 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900832
833 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700834 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900835
836 private final Resources mRes;
837 private final ContentResolver mResolver;
838 private final HashMap<String, InputMethodInfo> mMethodMap;
839 private final ArrayList<InputMethodInfo> mMethodList;
840
Yohei Yukawa68645a62016-02-17 07:54:20 -0800841 /**
842 * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
843 */
844 private final HashMap<String, String> mCopyOnWriteDataStore = new HashMap<>();
845
846 private boolean mCopyOnWrite = false;
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700847 @NonNull
848 private String mEnabledInputMethodsStrCache = "";
Yohei Yukawa68645a62016-02-17 07:54:20 -0800849 @UserIdInt
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900850 private int mCurrentUserId;
Kenny Guy2a764942014-04-02 13:29:20 +0100851 private int[] mCurrentProfileIds = new int[0];
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900852
853 private static void buildEnabledInputMethodsSettingString(
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700854 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
855 builder.append(ime.first);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900856 // Inputmethod and subtypes are saved in the settings as follows:
857 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700858 for (String subtypeId: ime.second) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700859 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900860 }
861 }
862
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700863 public static String buildInputMethodsSettingString(
864 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
865 final StringBuilder b = new StringBuilder();
866 boolean needsSeparator = false;
867 for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
868 if (needsSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700869 b.append(INPUT_METHOD_SEPARATOR);
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700870 }
871 buildEnabledInputMethodsSettingString(b, ime);
872 needsSeparator = true;
873 }
874 return b.toString();
875 }
876
877 public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
878 String enabledInputMethodsStr,
879 TextUtils.SimpleStringSplitter inputMethodSplitter,
880 TextUtils.SimpleStringSplitter subtypeSplitter) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700881 ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700882 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
883 return imsList;
884 }
885 inputMethodSplitter.setString(enabledInputMethodsStr);
886 while (inputMethodSplitter.hasNext()) {
887 String nextImsStr = inputMethodSplitter.next();
888 subtypeSplitter.setString(nextImsStr);
889 if (subtypeSplitter.hasNext()) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700890 ArrayList<String> subtypeHashes = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700891 // The first element is ime id.
892 String imeId = subtypeSplitter.next();
893 while (subtypeSplitter.hasNext()) {
894 subtypeHashes.add(subtypeSplitter.next());
895 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700896 imsList.add(new Pair<>(imeId, subtypeHashes));
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700897 }
898 }
899 return imsList;
900 }
901
Yohei Yukawa68645a62016-02-17 07:54:20 -0800902 public InputMethodSettings(
903 Resources res, ContentResolver resolver,
904 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
905 @UserIdInt int userId, boolean copyOnWrite) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900906 mRes = res;
907 mResolver = resolver;
908 mMethodMap = methodMap;
909 mMethodList = methodList;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800910 switchCurrentUser(userId, copyOnWrite);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900911 }
912
Yohei Yukawa68645a62016-02-17 07:54:20 -0800913 /**
914 * Must be called when the current user is changed.
915 *
916 * @param userId The user ID.
917 * @param copyOnWrite If {@code true}, for each settings key
918 * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
919 * settings on the {@link Settings.Secure} until we do the first write operation.
920 */
921 public void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900922 if (DEBUG) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800923 Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900924 }
Yohei Yukawa68645a62016-02-17 07:54:20 -0800925 if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
926 mCopyOnWriteDataStore.clear();
927 mEnabledInputMethodsStrCache = "";
928 // TODO: mCurrentProfileIds should be cleared here.
929 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900930 mCurrentUserId = userId;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800931 mCopyOnWrite = copyOnWrite;
932 // TODO: mCurrentProfileIds should be updated here.
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900933 }
934
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700935 private void putString(@NonNull final String key, @Nullable final String str) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800936 if (mCopyOnWrite) {
937 mCopyOnWriteDataStore.put(key, str);
938 } else {
939 Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId);
940 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800941 }
942
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700943 @Nullable
944 private String getString(@NonNull final String key, @Nullable final String defaultValue) {
945 final String result;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800946 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700947 result = mCopyOnWriteDataStore.get(key);
948 } else {
949 result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
Yohei Yukawa68645a62016-02-17 07:54:20 -0800950 }
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700951 return result != null ? result : defaultValue;
Yohei Yukawa87523672016-02-12 19:37:08 -0800952 }
953
954 private void putInt(final String key, final int value) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800955 if (mCopyOnWrite) {
956 mCopyOnWriteDataStore.put(key, String.valueOf(value));
957 } else {
958 Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId);
959 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800960 }
961
962 private int getInt(final String key, final int defaultValue) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800963 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
964 final String result = mCopyOnWriteDataStore.get(key);
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100965 return result != null ? Integer.parseInt(result) : 0;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800966 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800967 return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
968 }
969
970 private void putBoolean(final String key, final boolean value) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800971 putInt(key, value ? 1 : 0);
Yohei Yukawa87523672016-02-12 19:37:08 -0800972 }
973
974 private boolean getBoolean(final String key, final boolean defaultValue) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800975 return getInt(key, defaultValue ? 1 : 0) == 1;
Yohei Yukawa87523672016-02-12 19:37:08 -0800976 }
977
Kenny Guy2a764942014-04-02 13:29:20 +0100978 public void setCurrentProfileIds(int[] currentProfileIds) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800979 synchronized (this) {
Kenny Guy2a764942014-04-02 13:29:20 +0100980 mCurrentProfileIds = currentProfileIds;
Amith Yamasani734983f2014-03-04 16:48:05 -0800981 }
982 }
983
Kenny Guy2a764942014-04-02 13:29:20 +0100984 public boolean isCurrentProfile(int userId) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800985 synchronized (this) {
Kenny Guyf4824a02014-04-02 19:17:41 +0100986 if (userId == mCurrentUserId) return true;
Kenny Guy2a764942014-04-02 13:29:20 +0100987 for (int i = 0; i < mCurrentProfileIds.length; i++) {
988 if (userId == mCurrentProfileIds[i]) return true;
Amith Yamasani734983f2014-03-04 16:48:05 -0800989 }
990 return false;
991 }
992 }
993
Yohei Yukawac2393ac2016-02-18 00:30:45 -0800994 public ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900995 return createEnabledInputMethodListLocked(
996 getEnabledInputMethodsAndSubtypeListLocked());
997 }
998
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900999 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
1000 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
1001 List<InputMethodSubtype> enabledSubtypes =
1002 getEnabledInputMethodSubtypeListLocked(imi);
1003 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
1004 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
1005 context.getResources(), imi);
1006 }
1007 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
1008 }
1009
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +09001010 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001011 InputMethodInfo imi) {
1012 List<Pair<String, ArrayList<String>>> imsList =
1013 getEnabledInputMethodsAndSubtypeListLocked();
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001014 ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001015 if (imi != null) {
1016 for (Pair<String, ArrayList<String>> imsPair : imsList) {
1017 InputMethodInfo info = mMethodMap.get(imsPair.first);
1018 if (info != null && info.getId().equals(imi.getId())) {
1019 final int subtypeCount = info.getSubtypeCount();
1020 for (int i = 0; i < subtypeCount; ++i) {
1021 InputMethodSubtype ims = info.getSubtypeAt(i);
1022 for (String s: imsPair.second) {
1023 if (String.valueOf(ims.hashCode()).equals(s)) {
1024 enabledSubtypes.add(ims);
1025 }
1026 }
1027 }
1028 break;
1029 }
1030 }
1031 }
1032 return enabledSubtypes;
1033 }
1034
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001035 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
Christopher Tate7b9a28c2015-03-18 13:06:16 -07001036 return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
1037 mInputMethodSplitter,
1038 mSubtypeSplitter);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001039 }
1040
1041 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
1042 if (reloadInputMethodStr) {
1043 getEnabledInputMethodsStr();
1044 }
1045 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
1046 // Add in the newly enabled input method.
1047 putEnabledInputMethodsStr(id);
1048 } else {
1049 putEnabledInputMethodsStr(
Seigo Nonakace2c7842015-08-17 08:47:36 -07001050 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001051 }
1052 }
1053
1054 /**
1055 * Build and put a string of EnabledInputMethods with removing specified Id.
1056 * @return the specified id was removed or not.
1057 */
1058 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
1059 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
1060 boolean isRemoved = false;
1061 boolean needsAppendSeparator = false;
1062 for (Pair<String, ArrayList<String>> ims: imsList) {
1063 String curId = ims.first;
1064 if (curId.equals(id)) {
1065 // We are disabling this input method, and it is
1066 // currently enabled. Skip it to remove from the
1067 // new list.
1068 isRemoved = true;
1069 } else {
1070 if (needsAppendSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001071 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001072 } else {
1073 needsAppendSeparator = true;
1074 }
1075 buildEnabledInputMethodsSettingString(builder, ims);
1076 }
1077 }
1078 if (isRemoved) {
1079 // Update the setting with the new list of input methods.
1080 putEnabledInputMethodsStr(builder.toString());
1081 }
1082 return isRemoved;
1083 }
1084
Yohei Yukawac2393ac2016-02-18 00:30:45 -08001085 private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001086 List<Pair<String, ArrayList<String>>> imsList) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001087 final ArrayList<InputMethodInfo> res = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001088 for (Pair<String, ArrayList<String>> ims: imsList) {
1089 InputMethodInfo info = mMethodMap.get(ims.first);
1090 if (info != null) {
1091 res.add(info);
1092 }
1093 }
1094 return res;
1095 }
1096
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001097 private void putEnabledInputMethodsStr(@Nullable String str) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001098 if (DEBUG) {
1099 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1100 }
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001101 if (TextUtils.isEmpty(str)) {
1102 // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
1103 // empty data scenario.
1104 putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
1105 } else {
1106 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
1107 }
1108 // TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
1109 mEnabledInputMethodsStrCache = (str != null ? str : "");
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001110 }
1111
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001112 @NonNull
Dianne Hackbornfd7aded2013-01-22 17:10:23 -08001113 public String getEnabledInputMethodsStr() {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001114 mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001115 if (DEBUG) {
1116 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1117 + ", " + mCurrentUserId);
1118 }
1119 return mEnabledInputMethodsStrCache;
1120 }
1121
1122 private void saveSubtypeHistory(
1123 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1124 StringBuilder builder = new StringBuilder();
1125 boolean isImeAdded = false;
1126 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001127 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001128 newSubtypeId);
1129 isImeAdded = true;
1130 }
1131 for (Pair<String, String> ime: savedImes) {
1132 String imeId = ime.first;
1133 String subtypeId = ime.second;
1134 if (TextUtils.isEmpty(subtypeId)) {
1135 subtypeId = NOT_A_SUBTYPE_ID_STR;
1136 }
1137 if (isImeAdded) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001138 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001139 } else {
1140 isImeAdded = true;
1141 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001142 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001143 subtypeId);
1144 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001145 // Remove the last INPUT_METHOD_SEPARATOR
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001146 putSubtypeHistoryStr(builder.toString());
1147 }
1148
1149 private void addSubtypeToHistory(String imeId, String subtypeId) {
1150 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1151 for (Pair<String, String> ime: subtypeHistory) {
1152 if (ime.first.equals(imeId)) {
1153 if (DEBUG) {
1154 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1155 + ime.second);
1156 }
1157 // We should break here
1158 subtypeHistory.remove(ime);
1159 break;
1160 }
1161 }
1162 if (DEBUG) {
1163 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1164 }
1165 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1166 }
1167
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001168 private void putSubtypeHistoryStr(@NonNull String str) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001169 if (DEBUG) {
1170 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1171 }
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001172 if (TextUtils.isEmpty(str)) {
1173 // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
1174 // data scenario.
1175 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
1176 } else {
1177 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
1178 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001179 }
1180
1181 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1182 // Gets the first one from the history
1183 return getLastSubtypeForInputMethodLockedInternal(null);
1184 }
1185
1186 public String getLastSubtypeForInputMethodLocked(String imeId) {
1187 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1188 if (ime != null) {
1189 return ime.second;
1190 } else {
1191 return null;
1192 }
1193 }
1194
1195 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1196 List<Pair<String, ArrayList<String>>> enabledImes =
1197 getEnabledInputMethodsAndSubtypeListLocked();
1198 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1199 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1200 final String imeInTheHistory = imeAndSubtype.first;
1201 // If imeId is empty, returns the first IME and subtype in the history
1202 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1203 final String subtypeInTheHistory = imeAndSubtype.second;
1204 final String subtypeHashCode =
1205 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1206 enabledImes, imeInTheHistory, subtypeInTheHistory);
1207 if (!TextUtils.isEmpty(subtypeHashCode)) {
1208 if (DEBUG) {
1209 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1210 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001211 return new Pair<>(imeInTheHistory, subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001212 }
1213 }
1214 }
1215 if (DEBUG) {
1216 Slog.d(TAG, "No enabled IME found in the history");
1217 }
1218 return null;
1219 }
1220
1221 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1222 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1223 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1224 if (enabledIme.first.equals(imeId)) {
1225 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1226 final InputMethodInfo imi = mMethodMap.get(imeId);
1227 if (explicitlyEnabledSubtypes.size() == 0) {
1228 // If there are no explicitly enabled subtypes, applicable subtypes are
1229 // enabled implicitly.
1230 // If IME is enabled and no subtypes are enabled, applicable subtypes
1231 // are enabled implicitly, so needs to treat them to be enabled.
1232 if (imi != null && imi.getSubtypeCount() > 0) {
1233 List<InputMethodSubtype> implicitlySelectedSubtypes =
1234 getImplicitlyApplicableSubtypesLocked(mRes, imi);
1235 if (implicitlySelectedSubtypes != null) {
1236 final int N = implicitlySelectedSubtypes.size();
1237 for (int i = 0; i < N; ++i) {
1238 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1239 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1240 return subtypeHashCode;
1241 }
1242 }
1243 }
1244 }
1245 } else {
1246 for (String s: explicitlyEnabledSubtypes) {
1247 if (s.equals(subtypeHashCode)) {
1248 // If both imeId and subtypeId are enabled, return subtypeId.
1249 try {
Narayan Kamatha09b4d22016-04-15 18:32:45 +01001250 final int hashCode = Integer.parseInt(subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001251 // Check whether the subtype id is valid or not
1252 if (isValidSubtypeId(imi, hashCode)) {
1253 return s;
1254 } else {
1255 return NOT_A_SUBTYPE_ID_STR;
1256 }
1257 } catch (NumberFormatException e) {
1258 return NOT_A_SUBTYPE_ID_STR;
1259 }
1260 }
1261 }
1262 }
1263 // If imeId was enabled but subtypeId was disabled.
1264 return NOT_A_SUBTYPE_ID_STR;
1265 }
1266 }
1267 // If both imeId and subtypeId are disabled, return null
1268 return null;
1269 }
1270
1271 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001272 ArrayList<Pair<String, String>> imsList = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001273 final String subtypeHistoryStr = getSubtypeHistoryStr();
1274 if (TextUtils.isEmpty(subtypeHistoryStr)) {
1275 return imsList;
1276 }
1277 mInputMethodSplitter.setString(subtypeHistoryStr);
1278 while (mInputMethodSplitter.hasNext()) {
1279 String nextImsStr = mInputMethodSplitter.next();
1280 mSubtypeSplitter.setString(nextImsStr);
1281 if (mSubtypeSplitter.hasNext()) {
1282 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1283 // The first element is ime id.
1284 String imeId = mSubtypeSplitter.next();
1285 while (mSubtypeSplitter.hasNext()) {
1286 subtypeId = mSubtypeSplitter.next();
1287 break;
1288 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001289 imsList.add(new Pair<>(imeId, subtypeId));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001290 }
1291 }
1292 return imsList;
1293 }
1294
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001295 @NonNull
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001296 private String getSubtypeHistoryStr() {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001297 final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001298 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001299 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001300 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001301 return history;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001302 }
1303
1304 public void putSelectedInputMethod(String imeId) {
1305 if (DEBUG) {
1306 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1307 + mCurrentUserId);
1308 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001309 putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001310 }
1311
1312 public void putSelectedSubtype(int subtypeId) {
1313 if (DEBUG) {
1314 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1315 + mCurrentUserId);
1316 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001317 putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001318 }
1319
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001320 @Nullable
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001321 public String getSelectedInputMethod() {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001322 final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001323 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001324 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001325 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001326 return imi;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001327 }
1328
1329 public boolean isSubtypeSelected() {
1330 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1331 }
1332
1333 private int getSelectedInputMethodSubtypeHashCode() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001334 return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001335 }
1336
Michael Wright7b5a96b2014-08-09 19:28:42 -07001337 public boolean isShowImeWithHardKeyboardEnabled() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001338 return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001339 }
1340
1341 public void setShowImeWithHardKeyboard(boolean show) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001342 putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001343 }
1344
Yohei Yukawa68645a62016-02-17 07:54:20 -08001345 @UserIdInt
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001346 public int getCurrentUserId() {
1347 return mCurrentUserId;
1348 }
1349
1350 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
1351 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1352 if (imi == null) {
1353 return NOT_A_SUBTYPE_ID;
1354 }
1355 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1356 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1357 }
1358
1359 public void saveCurrentInputMethodAndSubtypeToHistory(
1360 String curMethodId, InputMethodSubtype currentSubtype) {
1361 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1362 if (currentSubtype != null) {
1363 subtypeId = String.valueOf(currentSubtype.hashCode());
1364 }
1365 if (canAddToLastInputMethod(currentSubtype)) {
1366 addSubtypeToHistory(curMethodId, subtypeId);
1367 }
1368 }
Satoshi Kataokad787f692013-10-26 04:44:21 +09001369
1370 public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1371 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1372 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001373 new HashMap<>();
Satoshi Kataokad787f692013-10-26 04:44:21 +09001374 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1375 enabledInputMethodAndSubtypes.put(
1376 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1377 }
1378 return enabledInputMethodAndSubtypes;
1379 }
Yohei Yukawa68645a62016-02-17 07:54:20 -08001380
1381 public void dumpLocked(final Printer pw, final String prefix) {
1382 pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
1383 pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
1384 pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
1385 pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
1386 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001387 }
Yohei Yukawa174843a2015-06-26 18:02:54 -07001388
1389 // For spell checker service manager.
1390 // TODO: Should we have TextServicesUtils.java?
1391 private static final Locale LOCALE_EN_US = new Locale("en", "US");
1392 private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
1393
1394 /**
1395 * Returns a list of {@link Locale} in the order of appropriateness for the default spell
1396 * checker service.
1397 *
1398 * <p>If the system language is English, and the region is also explicitly specified in the
1399 * system locale, the following fallback order will be applied.</p>
1400 * <ul>
1401 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1402 * <li>(system-locale-language, system-locale-region)</li>
1403 * <li>("en", "US")</li>
1404 * <li>("en", "GB")</li>
1405 * <li>("en")</li>
1406 * </ul>
1407 *
1408 * <p>If the system language is English, but no region is specified in the system locale,
1409 * the following fallback order will be applied.</p>
1410 * <ul>
1411 * <li>("en")</li>
1412 * <li>("en", "US")</li>
1413 * <li>("en", "GB")</li>
1414 * </ul>
1415 *
1416 * <p>If the system language is not English, the following fallback order will be applied.</p>
1417 * <ul>
1418 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1419 * <li>(system-locale-language, system-locale-region) (if exists)</li>
1420 * <li>(system-locale-language) (if exists)</li>
1421 * <li>("en", "US")</li>
1422 * <li>("en", "GB")</li>
1423 * <li>("en")</li>
1424 * </ul>
1425 *
1426 * @param systemLocale the current system locale to be taken into consideration.
1427 * @return a list of {@link Locale}. The first one is considered to be most appropriate.
1428 */
1429 @VisibleForTesting
1430 public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
1431 @Nullable final Locale systemLocale) {
1432 final Locale systemLocaleLanguageCountryVariant;
1433 final Locale systemLocaleLanguageCountry;
1434 final Locale systemLocaleLanguage;
1435 if (systemLocale != null) {
1436 final String language = systemLocale.getLanguage();
1437 final boolean hasLanguage = !TextUtils.isEmpty(language);
1438 final String country = systemLocale.getCountry();
1439 final boolean hasCountry = !TextUtils.isEmpty(country);
1440 final String variant = systemLocale.getVariant();
1441 final boolean hasVariant = !TextUtils.isEmpty(variant);
1442 if (hasLanguage && hasCountry && hasVariant) {
1443 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
1444 } else {
1445 systemLocaleLanguageCountryVariant = null;
1446 }
1447 if (hasLanguage && hasCountry) {
1448 systemLocaleLanguageCountry = new Locale(language, country);
1449 } else {
1450 systemLocaleLanguageCountry = null;
1451 }
1452 if (hasLanguage) {
1453 systemLocaleLanguage = new Locale(language);
1454 } else {
1455 systemLocaleLanguage = null;
1456 }
1457 } else {
1458 systemLocaleLanguageCountryVariant = null;
1459 systemLocaleLanguageCountry = null;
1460 systemLocaleLanguage = null;
1461 }
1462
1463 final ArrayList<Locale> locales = new ArrayList<>();
1464 if (systemLocaleLanguageCountryVariant != null) {
1465 locales.add(systemLocaleLanguageCountryVariant);
1466 }
1467
1468 if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
1469 if (systemLocaleLanguageCountry != null) {
1470 // If the system language is English, and the region is also explicitly specified,
1471 // following fallback order will be applied.
1472 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
1473 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
1474 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
1475 // - en
1476 if (systemLocaleLanguageCountry != null) {
1477 locales.add(systemLocaleLanguageCountry);
1478 }
1479 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
1480 locales.add(LOCALE_EN_US);
1481 }
1482 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
1483 locales.add(LOCALE_EN_GB);
1484 }
1485 locales.add(Locale.ENGLISH);
1486 } else {
1487 // If the system language is English, but no region is specified, following
1488 // fallback order will be applied.
1489 // - en
1490 // - en_US
1491 // - en_GB
1492 locales.add(Locale.ENGLISH);
1493 locales.add(LOCALE_EN_US);
1494 locales.add(LOCALE_EN_GB);
1495 }
1496 } else {
1497 // If the system language is not English, the fallback order will be
1498 // - systemLocaleLanguageCountry [if non-null]
1499 // - systemLocaleLanguage [if non-null]
1500 // - en_US
1501 // - en_GB
1502 // - en
1503 if (systemLocaleLanguageCountry != null) {
1504 locales.add(systemLocaleLanguageCountry);
1505 }
1506 if (systemLocaleLanguage != null) {
1507 locales.add(systemLocaleLanguage);
1508 }
1509 locales.add(LOCALE_EN_US);
1510 locales.add(LOCALE_EN_GB);
1511 locales.add(Locale.ENGLISH);
1512 }
1513 return locales;
1514 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001515}