blob: 716997f815dc4d6760a01c2f8b74cb2917f6fe86 [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 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);
Yohei Yukawaed65bc02015-12-02 18:22:41 -0800388 if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
389 // Convert a locale whose language is "tl" to one whose language is "fil".
390 // For example, "tl_PH" will get converted to "fil_PH".
391 // Versions of Android earlier than Lollipop did not support three letter language
392 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
393 // On Lollipop and above, the current three letter version must be used.
394 localeParams[0] = "fil";
395 }
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900396 // The length of localeStr is guaranteed to always return a 1 <= value <= 3
397 // because localeStr is not empty.
398 if (localeParams.length == 1) {
399 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 Yukawae985c242016-02-24 18:27:04 -0800502 private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
503 new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
504 @Override
505 public Locale get(InputMethodSubtype source) {
506 return source != null ? source.getLocaleObject() : null;
507 }
508 };
509
Yohei Yukawae72d1c82015-02-20 20:55:21 +0900510 @VisibleForTesting
511 public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900512 Resources res, InputMethodInfo imi) {
Yohei Yukawaccb024a2016-06-13 22:16:52 -0700513 final LocaleList systemLocales = res.getConfiguration().getLocales();
514
515 synchronized (sCacheLock) {
516 // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
517 // it does not check if subtypes are also identical.
518 if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
519 return new ArrayList<>(sCachedResult);
520 }
521 }
522
523 // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
524 // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
525 // LocaleList rather than Resource.
526 final ArrayList<InputMethodSubtype> result =
527 getImplicitlyApplicableSubtypesLockedImpl(res, imi);
528 synchronized (sCacheLock) {
529 // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
530 sCachedSystemLocales = systemLocales;
531 sCachedInputMethodInfo = imi;
532 sCachedResult = new ArrayList<>(result);
533 }
534 return result;
535 }
536
537 private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
538 Resources res, InputMethodInfo imi) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900539 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
Yohei Yukawae985c242016-02-24 18:27:04 -0800540 final LocaleList systemLocales = res.getConfiguration().getLocales();
541 final String systemLocale = systemLocales.get(0).toString();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700542 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800543 final int numSubtypes = subtypes.size();
544
545 // Handle overridesImplicitlyEnabledSubtype mechanism.
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700546 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800547 for (int i = 0; i < numSubtypes; ++i) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900548 // scan overriding implicitly enabled subtypes.
Yohei Yukawae985c242016-02-24 18:27:04 -0800549 final InputMethodSubtype subtype = subtypes.get(i);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900550 if (subtype.overridesImplicitlyEnabledSubtype()) {
551 final String mode = subtype.getMode();
552 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
553 applicableModeAndSubtypesMap.put(mode, subtype);
554 }
555 }
556 }
557 if (applicableModeAndSubtypesMap.size() > 0) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700558 return new ArrayList<>(applicableModeAndSubtypesMap.values());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900559 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800560
Yohei Yukawa238faad2016-03-11 10:39:51 -0800561 final HashMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
562 new HashMap<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800563 final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
Yohei Yukawa238faad2016-03-11 10:39:51 -0800564
Yohei Yukawae985c242016-02-24 18:27:04 -0800565 for (int i = 0; i < numSubtypes; ++i) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900566 final InputMethodSubtype subtype = subtypes.get(i);
Yohei Yukawa238faad2016-03-11 10:39:51 -0800567 final String mode = subtype.getMode();
568 if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
Yohei Yukawae985c242016-02-24 18:27:04 -0800569 keyboardSubtypes.add(subtype);
570 } else {
Yohei Yukawa238faad2016-03-11 10:39:51 -0800571 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
572 nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900573 }
Yohei Yukawa238faad2016-03-11 10:39:51 -0800574 nonKeyboardSubtypesMap.get(mode).add(subtype);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900575 }
576 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800577
578 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
579 LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
580 applicableSubtypes);
581
Yohei Yukawa42275bc2016-03-03 00:34:27 -0800582 if (!applicableSubtypes.isEmpty()) {
583 boolean hasAsciiCapableKeyboard = false;
584 final int numApplicationSubtypes = applicableSubtypes.size();
585 for (int i = 0; i < numApplicationSubtypes; ++i) {
586 final InputMethodSubtype subtype = applicableSubtypes.get(i);
587 if (subtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
588 hasAsciiCapableKeyboard = true;
589 break;
590 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800591 }
Yohei Yukawa42275bc2016-03-03 00:34:27 -0800592 if (!hasAsciiCapableKeyboard) {
593 final int numKeyboardSubtypes = keyboardSubtypes.size();
594 for (int i = 0; i < numKeyboardSubtypes; ++i) {
595 final InputMethodSubtype subtype = keyboardSubtypes.get(i);
596 final String mode = subtype.getMode();
597 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
598 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
599 applicableSubtypes.add(subtype);
600 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900601 }
602 }
603 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800604
605 if (applicableSubtypes.isEmpty()) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900606 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
607 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
608 if (lastResortKeyboardSubtype != null) {
609 applicableSubtypes.add(lastResortKeyboardSubtype);
610 }
611 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800612
Yohei Yukawa238faad2016-03-11 10:39:51 -0800613 // For each non-keyboard mode, extract subtypes with system locales.
614 for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
615 LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
616 applicableSubtypes);
617 }
618
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900619 return applicableSubtypes;
620 }
621
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900622 /**
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100623 * Returns the language component of a given locale string.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900624 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100625 */
Tadashi G. Takaoka77cbcb62014-07-12 16:08:20 +0900626 public static String getLanguageFromLocaleString(String locale) {
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100627 final int idx = locale.indexOf('_');
628 if (idx < 0) {
629 return locale;
630 } else {
631 return locale.substring(0, idx);
632 }
633 }
634
635 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900636 * If there are no selected subtypes, tries finding the most applicable one according to the
637 * given locale.
638 * @param subtypes this function will search the most applicable subtype in subtypes
639 * @param mode subtypes will be filtered by mode
640 * @param locale subtypes will be filtered by locale
641 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
642 * it will return the first subtype matched with mode
643 * @return the most applicable subtypeId
644 */
645 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
646 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
647 boolean canIgnoreLocaleAsLastResort) {
648 if (subtypes == null || subtypes.size() == 0) {
649 return null;
650 }
651 if (TextUtils.isEmpty(locale)) {
652 locale = res.getConfiguration().locale.toString();
653 }
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100654 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900655 boolean partialMatchFound = false;
656 InputMethodSubtype applicableSubtype = null;
657 InputMethodSubtype firstMatchedModeSubtype = null;
658 final int N = subtypes.size();
659 for (int i = 0; i < N; ++i) {
660 InputMethodSubtype subtype = subtypes.get(i);
661 final String subtypeLocale = subtype.getLocale();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100662 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900663 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
664 // and all subtypes with all modes can be candidates.
665 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
666 if (firstMatchedModeSubtype == null) {
667 firstMatchedModeSubtype = subtype;
668 }
669 if (locale.equals(subtypeLocale)) {
670 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
671 applicableSubtype = subtype;
672 break;
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100673 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900674 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
675 applicableSubtype = subtype;
676 partialMatchFound = true;
677 }
678 }
679 }
680
681 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
682 return firstMatchedModeSubtype;
683 }
684
685 // The first subtype applicable to the system locale will be defined as the most applicable
686 // subtype.
687 if (DEBUG) {
688 if (applicableSubtype != null) {
689 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
690 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
691 }
692 }
693 return applicableSubtype;
694 }
695
696 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
697 if (subtype == null) return true;
698 return !subtype.isAuxiliary();
699 }
700
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900701 public static void setNonSelectedSystemImesDisabledUntilUsed(
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700702 IPackageManager packageManager, List<InputMethodInfo> enabledImis,
703 int userId, String callingPackage) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900704 if (DEBUG) {
705 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
706 }
707 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
708 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
709 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
710 return;
711 }
712 // Only the current spell checker should be treated as an enabled one.
713 final SpellCheckerInfo currentSpellChecker =
714 TextServicesManager.getInstance().getCurrentSpellChecker();
715 for (final String packageName : systemImesDisabledUntilUsed) {
716 if (DEBUG) {
717 Slog.d(TAG, "check " + packageName);
718 }
719 boolean enabledIme = false;
720 for (int j = 0; j < enabledImis.size(); ++j) {
721 final InputMethodInfo imi = enabledImis.get(j);
722 if (packageName.equals(imi.getPackageName())) {
723 enabledIme = true;
724 break;
725 }
726 }
727 if (enabledIme) {
728 // enabled ime. skip
729 continue;
730 }
731 if (currentSpellChecker != null
732 && packageName.equals(currentSpellChecker.getPackageName())) {
733 // enabled spell checker. skip
734 if (DEBUG) {
735 Slog.d(TAG, packageName + " is the current spell checker. skip");
736 }
737 continue;
738 }
739 ApplicationInfo ai = null;
740 try {
741 ai = packageManager.getApplicationInfo(packageName,
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700742 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
743 } catch (RemoteException e) {
744 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
745 + " userId=" + userId, e);
746 continue;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900747 }
748 if (ai == null) {
749 // No app found for packageName
750 continue;
751 }
752 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
753 if (!isSystemPackage) {
754 continue;
755 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700756 setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900757 }
758 }
759
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700760 private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
761 int userId, String callingPackage) {
762 final int state;
763 try {
764 state = packageManager.getApplicationEnabledSetting(packageName, userId);
765 } catch (RemoteException e) {
766 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
767 + " userId=" + userId, e);
768 return;
769 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900770 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
771 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
772 if (DEBUG) {
773 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
774 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700775 try {
776 packageManager.setApplicationEnabledSetting(packageName,
777 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
778 0 /* newState */, userId, callingPackage);
779 } catch (RemoteException e) {
780 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
781 + " userId=" + userId + " callingPackage=" + callingPackage, e);
782 return;
783 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900784 } else {
785 if (DEBUG) {
786 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
787 }
788 }
789 }
790
Satoshi Kataokab2827262013-07-04 19:43:14 +0900791 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
792 InputMethodSubtype subtype) {
793 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
794 return subtype != null
795 ? TextUtils.concat(subtype.getDisplayName(context,
796 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
797 (TextUtils.isEmpty(imiLabel) ?
798 "" : " - " + imiLabel))
799 : imiLabel;
800 }
801
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900802 /**
Yohei Yukawae63b5fa2014-09-19 13:14:55 +0900803 * Returns true if a package name belongs to a UID.
804 *
805 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
806 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
807 * @param uid the UID to be validated.
808 * @param packageName the package name.
809 * @return {@code true} if the package name belongs to the UID.
810 */
811 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
812 final int uid, final String packageName) {
813 try {
814 appOpsManager.checkPackage(uid, packageName);
815 return true;
816 } catch (SecurityException e) {
817 return false;
818 }
819 }
820
821 /**
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900822 * Parses the setting stored input methods and subtypes string value.
823 *
824 * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
825 * @return Map from input method ID to set of input method subtypes IDs.
826 */
827 @VisibleForTesting
828 public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
829 @Nullable final String inputMethodsAndSubtypesString) {
830
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800831 final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900832 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
833 return imeMap;
834 }
835
836 final SimpleStringSplitter typeSplitter =
837 new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
838 final SimpleStringSplitter subtypeSplitter =
839 new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
840
841 List<Pair<String, ArrayList<String>>> allImeSettings =
842 InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
843 typeSplitter,
844 subtypeSplitter);
845 for (Pair<String, ArrayList<String>> ime : allImeSettings) {
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800846 ArraySet<String> subtypes = new ArraySet<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900847 if (ime.second != null) {
848 subtypes.addAll(ime.second);
849 }
850 imeMap.put(ime.first, subtypes);
851 }
852 return imeMap;
853 }
854
Seigo Nonaka2a099bc2015-08-14 19:29:45 -0700855 @NonNull
856 public static String buildInputMethodsAndSubtypesString(
857 @NonNull final ArrayMap<String, ArraySet<String>> map) {
858 // we want to use the canonical InputMethodSettings implementation,
859 // so we convert data structures first.
860 List<Pair<String, ArrayList<String>>> imeMap = new ArrayList<>(4);
861 for (ArrayMap.Entry<String, ArraySet<String>> entry : map.entrySet()) {
862 final String imeName = entry.getKey();
863 final ArraySet<String> subtypeSet = entry.getValue();
864 final ArrayList<String> subtypes = new ArrayList<>(2);
865 if (subtypeSet != null) {
866 subtypes.addAll(subtypeSet);
867 }
868 imeMap.add(new Pair<>(imeName, subtypes));
869 }
870 return InputMethodSettings.buildInputMethodsSettingString(imeMap);
871 }
872
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900873 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900874 * Utility class for putting and getting settings for InputMethod
875 * TODO: Move all putters and getters of settings to this class.
876 */
877 public static class InputMethodSettings {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900878 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700879 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900880
881 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700882 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900883
884 private final Resources mRes;
885 private final ContentResolver mResolver;
886 private final HashMap<String, InputMethodInfo> mMethodMap;
887 private final ArrayList<InputMethodInfo> mMethodList;
888
Yohei Yukawa68645a62016-02-17 07:54:20 -0800889 /**
890 * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
891 */
892 private final HashMap<String, String> mCopyOnWriteDataStore = new HashMap<>();
893
894 private boolean mCopyOnWrite = false;
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700895 @NonNull
896 private String mEnabledInputMethodsStrCache = "";
Yohei Yukawa68645a62016-02-17 07:54:20 -0800897 @UserIdInt
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900898 private int mCurrentUserId;
Kenny Guy2a764942014-04-02 13:29:20 +0100899 private int[] mCurrentProfileIds = new int[0];
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900900
901 private static void buildEnabledInputMethodsSettingString(
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700902 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
903 builder.append(ime.first);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900904 // Inputmethod and subtypes are saved in the settings as follows:
905 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700906 for (String subtypeId: ime.second) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700907 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900908 }
909 }
910
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700911 public static String buildInputMethodsSettingString(
912 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
913 final StringBuilder b = new StringBuilder();
914 boolean needsSeparator = false;
915 for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
916 if (needsSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700917 b.append(INPUT_METHOD_SEPARATOR);
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700918 }
919 buildEnabledInputMethodsSettingString(b, ime);
920 needsSeparator = true;
921 }
922 return b.toString();
923 }
924
925 public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
926 String enabledInputMethodsStr,
927 TextUtils.SimpleStringSplitter inputMethodSplitter,
928 TextUtils.SimpleStringSplitter subtypeSplitter) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700929 ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700930 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
931 return imsList;
932 }
933 inputMethodSplitter.setString(enabledInputMethodsStr);
934 while (inputMethodSplitter.hasNext()) {
935 String nextImsStr = inputMethodSplitter.next();
936 subtypeSplitter.setString(nextImsStr);
937 if (subtypeSplitter.hasNext()) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700938 ArrayList<String> subtypeHashes = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700939 // The first element is ime id.
940 String imeId = subtypeSplitter.next();
941 while (subtypeSplitter.hasNext()) {
942 subtypeHashes.add(subtypeSplitter.next());
943 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700944 imsList.add(new Pair<>(imeId, subtypeHashes));
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700945 }
946 }
947 return imsList;
948 }
949
Yohei Yukawa68645a62016-02-17 07:54:20 -0800950 public InputMethodSettings(
951 Resources res, ContentResolver resolver,
952 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
953 @UserIdInt int userId, boolean copyOnWrite) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900954 mRes = res;
955 mResolver = resolver;
956 mMethodMap = methodMap;
957 mMethodList = methodList;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800958 switchCurrentUser(userId, copyOnWrite);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900959 }
960
Yohei Yukawa68645a62016-02-17 07:54:20 -0800961 /**
962 * Must be called when the current user is changed.
963 *
964 * @param userId The user ID.
965 * @param copyOnWrite If {@code true}, for each settings key
966 * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
967 * settings on the {@link Settings.Secure} until we do the first write operation.
968 */
969 public void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900970 if (DEBUG) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800971 Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900972 }
Yohei Yukawa68645a62016-02-17 07:54:20 -0800973 if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
974 mCopyOnWriteDataStore.clear();
975 mEnabledInputMethodsStrCache = "";
976 // TODO: mCurrentProfileIds should be cleared here.
977 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900978 mCurrentUserId = userId;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800979 mCopyOnWrite = copyOnWrite;
980 // TODO: mCurrentProfileIds should be updated here.
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900981 }
982
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700983 private void putString(@NonNull final String key, @Nullable final String str) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800984 if (mCopyOnWrite) {
985 mCopyOnWriteDataStore.put(key, str);
986 } else {
987 Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId);
988 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800989 }
990
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700991 @Nullable
992 private String getString(@NonNull final String key, @Nullable final String defaultValue) {
993 final String result;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800994 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700995 result = mCopyOnWriteDataStore.get(key);
996 } else {
997 result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
Yohei Yukawa68645a62016-02-17 07:54:20 -0800998 }
Yohei Yukawa7b574cb2016-03-16 17:22:22 -0700999 return result != null ? result : defaultValue;
Yohei Yukawa87523672016-02-12 19:37:08 -08001000 }
1001
1002 private void putInt(final String key, final int value) {
Yohei Yukawa68645a62016-02-17 07:54:20 -08001003 if (mCopyOnWrite) {
1004 mCopyOnWriteDataStore.put(key, String.valueOf(value));
1005 } else {
1006 Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId);
1007 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001008 }
1009
1010 private int getInt(final String key, final int defaultValue) {
Yohei Yukawa68645a62016-02-17 07:54:20 -08001011 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
1012 final String result = mCopyOnWriteDataStore.get(key);
Narayan Kamatha09b4d22016-04-15 18:32:45 +01001013 return result != null ? Integer.parseInt(result) : 0;
Yohei Yukawa68645a62016-02-17 07:54:20 -08001014 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001015 return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
1016 }
1017
1018 private void putBoolean(final String key, final boolean value) {
Yohei Yukawa68645a62016-02-17 07:54:20 -08001019 putInt(key, value ? 1 : 0);
Yohei Yukawa87523672016-02-12 19:37:08 -08001020 }
1021
1022 private boolean getBoolean(final String key, final boolean defaultValue) {
Yohei Yukawa68645a62016-02-17 07:54:20 -08001023 return getInt(key, defaultValue ? 1 : 0) == 1;
Yohei Yukawa87523672016-02-12 19:37:08 -08001024 }
1025
Kenny Guy2a764942014-04-02 13:29:20 +01001026 public void setCurrentProfileIds(int[] currentProfileIds) {
Amith Yamasani734983f2014-03-04 16:48:05 -08001027 synchronized (this) {
Kenny Guy2a764942014-04-02 13:29:20 +01001028 mCurrentProfileIds = currentProfileIds;
Amith Yamasani734983f2014-03-04 16:48:05 -08001029 }
1030 }
1031
Kenny Guy2a764942014-04-02 13:29:20 +01001032 public boolean isCurrentProfile(int userId) {
Amith Yamasani734983f2014-03-04 16:48:05 -08001033 synchronized (this) {
Kenny Guyf4824a02014-04-02 19:17:41 +01001034 if (userId == mCurrentUserId) return true;
Kenny Guy2a764942014-04-02 13:29:20 +01001035 for (int i = 0; i < mCurrentProfileIds.length; i++) {
1036 if (userId == mCurrentProfileIds[i]) return true;
Amith Yamasani734983f2014-03-04 16:48:05 -08001037 }
1038 return false;
1039 }
1040 }
1041
Yohei Yukawac2393ac2016-02-18 00:30:45 -08001042 public ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001043 return createEnabledInputMethodListLocked(
1044 getEnabledInputMethodsAndSubtypeListLocked());
1045 }
1046
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001047 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
1048 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
1049 List<InputMethodSubtype> enabledSubtypes =
1050 getEnabledInputMethodSubtypeListLocked(imi);
1051 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
1052 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
1053 context.getResources(), imi);
1054 }
1055 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
1056 }
1057
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +09001058 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001059 InputMethodInfo imi) {
1060 List<Pair<String, ArrayList<String>>> imsList =
1061 getEnabledInputMethodsAndSubtypeListLocked();
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001062 ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001063 if (imi != null) {
1064 for (Pair<String, ArrayList<String>> imsPair : imsList) {
1065 InputMethodInfo info = mMethodMap.get(imsPair.first);
1066 if (info != null && info.getId().equals(imi.getId())) {
1067 final int subtypeCount = info.getSubtypeCount();
1068 for (int i = 0; i < subtypeCount; ++i) {
1069 InputMethodSubtype ims = info.getSubtypeAt(i);
1070 for (String s: imsPair.second) {
1071 if (String.valueOf(ims.hashCode()).equals(s)) {
1072 enabledSubtypes.add(ims);
1073 }
1074 }
1075 }
1076 break;
1077 }
1078 }
1079 }
1080 return enabledSubtypes;
1081 }
1082
1083 // At the initial boot, the settings for input methods are not set,
1084 // so we need to enable IME in that case.
1085 public void enableAllIMEsIfThereIsNoEnabledIME() {
1086 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
1087 StringBuilder sb = new StringBuilder();
1088 final int N = mMethodList.size();
1089 for (int i = 0; i < N; i++) {
1090 InputMethodInfo imi = mMethodList.get(i);
1091 Slog.i(TAG, "Adding: " + imi.getId());
1092 if (i > 0) sb.append(':');
1093 sb.append(imi.getId());
1094 }
1095 putEnabledInputMethodsStr(sb.toString());
1096 }
1097 }
1098
1099 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
Christopher Tate7b9a28c2015-03-18 13:06:16 -07001100 return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
1101 mInputMethodSplitter,
1102 mSubtypeSplitter);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001103 }
1104
1105 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
1106 if (reloadInputMethodStr) {
1107 getEnabledInputMethodsStr();
1108 }
1109 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
1110 // Add in the newly enabled input method.
1111 putEnabledInputMethodsStr(id);
1112 } else {
1113 putEnabledInputMethodsStr(
Seigo Nonakace2c7842015-08-17 08:47:36 -07001114 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001115 }
1116 }
1117
1118 /**
1119 * Build and put a string of EnabledInputMethods with removing specified Id.
1120 * @return the specified id was removed or not.
1121 */
1122 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
1123 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
1124 boolean isRemoved = false;
1125 boolean needsAppendSeparator = false;
1126 for (Pair<String, ArrayList<String>> ims: imsList) {
1127 String curId = ims.first;
1128 if (curId.equals(id)) {
1129 // We are disabling this input method, and it is
1130 // currently enabled. Skip it to remove from the
1131 // new list.
1132 isRemoved = true;
1133 } else {
1134 if (needsAppendSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001135 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001136 } else {
1137 needsAppendSeparator = true;
1138 }
1139 buildEnabledInputMethodsSettingString(builder, ims);
1140 }
1141 }
1142 if (isRemoved) {
1143 // Update the setting with the new list of input methods.
1144 putEnabledInputMethodsStr(builder.toString());
1145 }
1146 return isRemoved;
1147 }
1148
Yohei Yukawac2393ac2016-02-18 00:30:45 -08001149 private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001150 List<Pair<String, ArrayList<String>>> imsList) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001151 final ArrayList<InputMethodInfo> res = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001152 for (Pair<String, ArrayList<String>> ims: imsList) {
1153 InputMethodInfo info = mMethodMap.get(ims.first);
1154 if (info != null) {
1155 res.add(info);
1156 }
1157 }
1158 return res;
1159 }
1160
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001161 private void putEnabledInputMethodsStr(@Nullable String str) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001162 if (DEBUG) {
1163 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1164 }
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001165 if (TextUtils.isEmpty(str)) {
1166 // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
1167 // empty data scenario.
1168 putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
1169 } else {
1170 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
1171 }
1172 // TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
1173 mEnabledInputMethodsStrCache = (str != null ? str : "");
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001174 }
1175
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001176 @NonNull
Dianne Hackbornfd7aded2013-01-22 17:10:23 -08001177 public String getEnabledInputMethodsStr() {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001178 mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001179 if (DEBUG) {
1180 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1181 + ", " + mCurrentUserId);
1182 }
1183 return mEnabledInputMethodsStrCache;
1184 }
1185
1186 private void saveSubtypeHistory(
1187 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1188 StringBuilder builder = new StringBuilder();
1189 boolean isImeAdded = false;
1190 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001191 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001192 newSubtypeId);
1193 isImeAdded = true;
1194 }
1195 for (Pair<String, String> ime: savedImes) {
1196 String imeId = ime.first;
1197 String subtypeId = ime.second;
1198 if (TextUtils.isEmpty(subtypeId)) {
1199 subtypeId = NOT_A_SUBTYPE_ID_STR;
1200 }
1201 if (isImeAdded) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001202 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001203 } else {
1204 isImeAdded = true;
1205 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001206 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001207 subtypeId);
1208 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001209 // Remove the last INPUT_METHOD_SEPARATOR
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001210 putSubtypeHistoryStr(builder.toString());
1211 }
1212
1213 private void addSubtypeToHistory(String imeId, String subtypeId) {
1214 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1215 for (Pair<String, String> ime: subtypeHistory) {
1216 if (ime.first.equals(imeId)) {
1217 if (DEBUG) {
1218 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1219 + ime.second);
1220 }
1221 // We should break here
1222 subtypeHistory.remove(ime);
1223 break;
1224 }
1225 }
1226 if (DEBUG) {
1227 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1228 }
1229 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1230 }
1231
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001232 private void putSubtypeHistoryStr(@NonNull String str) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001233 if (DEBUG) {
1234 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1235 }
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001236 if (TextUtils.isEmpty(str)) {
1237 // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
1238 // data scenario.
1239 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
1240 } else {
1241 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
1242 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001243 }
1244
1245 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1246 // Gets the first one from the history
1247 return getLastSubtypeForInputMethodLockedInternal(null);
1248 }
1249
1250 public String getLastSubtypeForInputMethodLocked(String imeId) {
1251 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1252 if (ime != null) {
1253 return ime.second;
1254 } else {
1255 return null;
1256 }
1257 }
1258
1259 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1260 List<Pair<String, ArrayList<String>>> enabledImes =
1261 getEnabledInputMethodsAndSubtypeListLocked();
1262 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1263 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1264 final String imeInTheHistory = imeAndSubtype.first;
1265 // If imeId is empty, returns the first IME and subtype in the history
1266 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1267 final String subtypeInTheHistory = imeAndSubtype.second;
1268 final String subtypeHashCode =
1269 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1270 enabledImes, imeInTheHistory, subtypeInTheHistory);
1271 if (!TextUtils.isEmpty(subtypeHashCode)) {
1272 if (DEBUG) {
1273 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1274 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001275 return new Pair<>(imeInTheHistory, subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001276 }
1277 }
1278 }
1279 if (DEBUG) {
1280 Slog.d(TAG, "No enabled IME found in the history");
1281 }
1282 return null;
1283 }
1284
1285 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1286 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1287 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1288 if (enabledIme.first.equals(imeId)) {
1289 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1290 final InputMethodInfo imi = mMethodMap.get(imeId);
1291 if (explicitlyEnabledSubtypes.size() == 0) {
1292 // If there are no explicitly enabled subtypes, applicable subtypes are
1293 // enabled implicitly.
1294 // If IME is enabled and no subtypes are enabled, applicable subtypes
1295 // are enabled implicitly, so needs to treat them to be enabled.
1296 if (imi != null && imi.getSubtypeCount() > 0) {
1297 List<InputMethodSubtype> implicitlySelectedSubtypes =
1298 getImplicitlyApplicableSubtypesLocked(mRes, imi);
1299 if (implicitlySelectedSubtypes != null) {
1300 final int N = implicitlySelectedSubtypes.size();
1301 for (int i = 0; i < N; ++i) {
1302 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1303 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1304 return subtypeHashCode;
1305 }
1306 }
1307 }
1308 }
1309 } else {
1310 for (String s: explicitlyEnabledSubtypes) {
1311 if (s.equals(subtypeHashCode)) {
1312 // If both imeId and subtypeId are enabled, return subtypeId.
1313 try {
Narayan Kamatha09b4d22016-04-15 18:32:45 +01001314 final int hashCode = Integer.parseInt(subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001315 // Check whether the subtype id is valid or not
1316 if (isValidSubtypeId(imi, hashCode)) {
1317 return s;
1318 } else {
1319 return NOT_A_SUBTYPE_ID_STR;
1320 }
1321 } catch (NumberFormatException e) {
1322 return NOT_A_SUBTYPE_ID_STR;
1323 }
1324 }
1325 }
1326 }
1327 // If imeId was enabled but subtypeId was disabled.
1328 return NOT_A_SUBTYPE_ID_STR;
1329 }
1330 }
1331 // If both imeId and subtypeId are disabled, return null
1332 return null;
1333 }
1334
1335 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001336 ArrayList<Pair<String, String>> imsList = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001337 final String subtypeHistoryStr = getSubtypeHistoryStr();
1338 if (TextUtils.isEmpty(subtypeHistoryStr)) {
1339 return imsList;
1340 }
1341 mInputMethodSplitter.setString(subtypeHistoryStr);
1342 while (mInputMethodSplitter.hasNext()) {
1343 String nextImsStr = mInputMethodSplitter.next();
1344 mSubtypeSplitter.setString(nextImsStr);
1345 if (mSubtypeSplitter.hasNext()) {
1346 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1347 // The first element is ime id.
1348 String imeId = mSubtypeSplitter.next();
1349 while (mSubtypeSplitter.hasNext()) {
1350 subtypeId = mSubtypeSplitter.next();
1351 break;
1352 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001353 imsList.add(new Pair<>(imeId, subtypeId));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001354 }
1355 }
1356 return imsList;
1357 }
1358
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001359 @NonNull
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001360 private String getSubtypeHistoryStr() {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001361 final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001362 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001363 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001364 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001365 return history;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001366 }
1367
1368 public void putSelectedInputMethod(String imeId) {
1369 if (DEBUG) {
1370 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1371 + mCurrentUserId);
1372 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001373 putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001374 }
1375
1376 public void putSelectedSubtype(int subtypeId) {
1377 if (DEBUG) {
1378 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1379 + mCurrentUserId);
1380 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001381 putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001382 }
1383
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001384 @Nullable
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001385 public String getSelectedInputMethod() {
Yohei Yukawa7b574cb2016-03-16 17:22:22 -07001386 final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001387 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001388 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001389 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001390 return imi;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001391 }
1392
1393 public boolean isSubtypeSelected() {
1394 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1395 }
1396
1397 private int getSelectedInputMethodSubtypeHashCode() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001398 return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001399 }
1400
Michael Wright7b5a96b2014-08-09 19:28:42 -07001401 public boolean isShowImeWithHardKeyboardEnabled() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001402 return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001403 }
1404
1405 public void setShowImeWithHardKeyboard(boolean show) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001406 putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001407 }
1408
Yohei Yukawa68645a62016-02-17 07:54:20 -08001409 @UserIdInt
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001410 public int getCurrentUserId() {
1411 return mCurrentUserId;
1412 }
1413
1414 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
1415 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1416 if (imi == null) {
1417 return NOT_A_SUBTYPE_ID;
1418 }
1419 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1420 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1421 }
1422
1423 public void saveCurrentInputMethodAndSubtypeToHistory(
1424 String curMethodId, InputMethodSubtype currentSubtype) {
1425 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1426 if (currentSubtype != null) {
1427 subtypeId = String.valueOf(currentSubtype.hashCode());
1428 }
1429 if (canAddToLastInputMethod(currentSubtype)) {
1430 addSubtypeToHistory(curMethodId, subtypeId);
1431 }
1432 }
Satoshi Kataokad787f692013-10-26 04:44:21 +09001433
1434 public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1435 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1436 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001437 new HashMap<>();
Satoshi Kataokad787f692013-10-26 04:44:21 +09001438 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1439 enabledInputMethodAndSubtypes.put(
1440 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1441 }
1442 return enabledInputMethodAndSubtypes;
1443 }
Yohei Yukawa68645a62016-02-17 07:54:20 -08001444
1445 public void dumpLocked(final Printer pw, final String prefix) {
1446 pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
1447 pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
1448 pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
1449 pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
1450 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001451 }
Yohei Yukawa174843a2015-06-26 18:02:54 -07001452
1453 // For spell checker service manager.
1454 // TODO: Should we have TextServicesUtils.java?
1455 private static final Locale LOCALE_EN_US = new Locale("en", "US");
1456 private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
1457
1458 /**
1459 * Returns a list of {@link Locale} in the order of appropriateness for the default spell
1460 * checker service.
1461 *
1462 * <p>If the system language is English, and the region is also explicitly specified in the
1463 * system locale, the following fallback order will be applied.</p>
1464 * <ul>
1465 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1466 * <li>(system-locale-language, system-locale-region)</li>
1467 * <li>("en", "US")</li>
1468 * <li>("en", "GB")</li>
1469 * <li>("en")</li>
1470 * </ul>
1471 *
1472 * <p>If the system language is English, but no region is specified in the system locale,
1473 * the following fallback order will be applied.</p>
1474 * <ul>
1475 * <li>("en")</li>
1476 * <li>("en", "US")</li>
1477 * <li>("en", "GB")</li>
1478 * </ul>
1479 *
1480 * <p>If the system language is not English, the following fallback order will be applied.</p>
1481 * <ul>
1482 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1483 * <li>(system-locale-language, system-locale-region) (if exists)</li>
1484 * <li>(system-locale-language) (if exists)</li>
1485 * <li>("en", "US")</li>
1486 * <li>("en", "GB")</li>
1487 * <li>("en")</li>
1488 * </ul>
1489 *
1490 * @param systemLocale the current system locale to be taken into consideration.
1491 * @return a list of {@link Locale}. The first one is considered to be most appropriate.
1492 */
1493 @VisibleForTesting
1494 public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
1495 @Nullable final Locale systemLocale) {
1496 final Locale systemLocaleLanguageCountryVariant;
1497 final Locale systemLocaleLanguageCountry;
1498 final Locale systemLocaleLanguage;
1499 if (systemLocale != null) {
1500 final String language = systemLocale.getLanguage();
1501 final boolean hasLanguage = !TextUtils.isEmpty(language);
1502 final String country = systemLocale.getCountry();
1503 final boolean hasCountry = !TextUtils.isEmpty(country);
1504 final String variant = systemLocale.getVariant();
1505 final boolean hasVariant = !TextUtils.isEmpty(variant);
1506 if (hasLanguage && hasCountry && hasVariant) {
1507 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
1508 } else {
1509 systemLocaleLanguageCountryVariant = null;
1510 }
1511 if (hasLanguage && hasCountry) {
1512 systemLocaleLanguageCountry = new Locale(language, country);
1513 } else {
1514 systemLocaleLanguageCountry = null;
1515 }
1516 if (hasLanguage) {
1517 systemLocaleLanguage = new Locale(language);
1518 } else {
1519 systemLocaleLanguage = null;
1520 }
1521 } else {
1522 systemLocaleLanguageCountryVariant = null;
1523 systemLocaleLanguageCountry = null;
1524 systemLocaleLanguage = null;
1525 }
1526
1527 final ArrayList<Locale> locales = new ArrayList<>();
1528 if (systemLocaleLanguageCountryVariant != null) {
1529 locales.add(systemLocaleLanguageCountryVariant);
1530 }
1531
1532 if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
1533 if (systemLocaleLanguageCountry != null) {
1534 // If the system language is English, and the region is also explicitly specified,
1535 // following fallback order will be applied.
1536 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
1537 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
1538 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
1539 // - en
1540 if (systemLocaleLanguageCountry != null) {
1541 locales.add(systemLocaleLanguageCountry);
1542 }
1543 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
1544 locales.add(LOCALE_EN_US);
1545 }
1546 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
1547 locales.add(LOCALE_EN_GB);
1548 }
1549 locales.add(Locale.ENGLISH);
1550 } else {
1551 // If the system language is English, but no region is specified, following
1552 // fallback order will be applied.
1553 // - en
1554 // - en_US
1555 // - en_GB
1556 locales.add(Locale.ENGLISH);
1557 locales.add(LOCALE_EN_US);
1558 locales.add(LOCALE_EN_GB);
1559 }
1560 } else {
1561 // If the system language is not English, the fallback order will be
1562 // - systemLocaleLanguageCountry [if non-null]
1563 // - systemLocaleLanguage [if non-null]
1564 // - en_US
1565 // - en_GB
1566 // - en
1567 if (systemLocaleLanguageCountry != null) {
1568 locales.add(systemLocaleLanguageCountry);
1569 }
1570 if (systemLocaleLanguage != null) {
1571 locales.add(systemLocaleLanguage);
1572 }
1573 locales.add(LOCALE_EN_US);
1574 locales.add(LOCALE_EN_GB);
1575 locales.add(Locale.ENGLISH);
1576 }
1577 return locales;
1578 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001579}