blob: 90ee05ec944b31cb17b302471a31b0cdefe758bc [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 Yukawa094c71f2015-06-20 00:41:31 -070029import android.os.RemoteException;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090030import android.provider.Settings;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090031import android.text.TextUtils;
Seigo Nonaka2028dda2015-07-06 17:41:24 +090032import android.text.TextUtils.SimpleStringSplitter;
33import android.util.ArrayMap;
34import android.util.ArraySet;
Yohei Yukawae985c242016-02-24 18:27:04 -080035import android.util.LocaleList;
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 Yukawae72d1c82015-02-20 20:55:21 +090044import com.android.internal.annotations.VisibleForTesting;
45
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090046import java.util.ArrayList;
Yohei Yukawab21220e2014-11-01 21:04:30 +090047import java.util.Arrays;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090048import java.util.HashMap;
Yohei Yukawab21220e2014-11-01 21:04:30 +090049import java.util.LinkedHashSet;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090050import java.util.List;
51import java.util.Locale;
52
53/**
54 * InputMethodManagerUtils contains some static methods that provides IME informations.
55 * This methods are supposed to be used in both the framework and the Settings application.
56 */
57public class InputMethodUtils {
58 public static final boolean DEBUG = false;
59 public static final int NOT_A_SUBTYPE_ID = -1;
Yohei Yukawa68c860b2014-09-13 22:03:37 +090060 public static final String SUBTYPE_MODE_ANY = null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090061 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
62 public static final String SUBTYPE_MODE_VOICE = "voice";
63 private static final String TAG = "InputMethodUtils";
64 private static final Locale ENGLISH_LOCALE = new Locale("en");
65 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
66 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
67 "EnabledWhenDefaultIsNotAsciiCapable";
68 private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
Seigo Nonakace2c7842015-08-17 08:47:36 -070069
70 // The string for enabled input method is saved as follows:
71 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
Seigo Nonaka2028dda2015-07-06 17:41:24 +090072 private static final char INPUT_METHOD_SEPARATOR = ':';
73 private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
Yohei Yukawadc489242014-09-14 12:01:59 +090074 /**
75 * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
76 * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
77 * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
78 * doesn't automatically match {@code Locale("en", "IN")}.
79 */
80 private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
81 Locale.ENGLISH, // "en"
82 Locale.US, // "en_US"
83 Locale.UK, // "en_GB"
84 };
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090085
86 private InputMethodUtils() {
87 // This utility class is not publicly instantiable.
88 }
89
Satoshi Kataoka0766eb02013-07-31 18:30:13 +090090 // ----------------------------------------------------------------------
91 // Utilities for debug
Satoshi Kataoka87c29142013-07-31 23:11:54 +090092 public static String getApiCallStack() {
93 String apiCallStack = "";
94 try {
95 throw new RuntimeException();
96 } catch (RuntimeException e) {
97 final StackTraceElement[] frames = e.getStackTrace();
98 for (int j = 1; j < frames.length; ++j) {
99 final String tempCallStack = frames[j].toString();
100 if (TextUtils.isEmpty(apiCallStack)) {
101 // Overwrite apiCallStack if it's empty
102 apiCallStack = tempCallStack;
103 } else if (tempCallStack.indexOf("Transact(") < 0) {
104 // Overwrite apiCallStack if it's not a binder call
105 apiCallStack = tempCallStack;
106 } else {
107 break;
108 }
109 }
110 }
111 return apiCallStack;
112 }
Satoshi Kataoka0766eb02013-07-31 18:30:13 +0900113 // ----------------------------------------------------------------------
114
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900115 public static boolean isSystemIme(InputMethodInfo inputMethod) {
116 return (inputMethod.getServiceInfo().applicationInfo.flags
117 & ApplicationInfo.FLAG_SYSTEM) != 0;
118 }
119
Yohei Yukawa9c83ff42015-03-12 15:31:25 +0900120 public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900121 final Context context, final boolean checkDefaultAttribute,
122 @Nullable final Locale requiredLocale, final boolean checkCountry,
123 final String requiredSubtypeMode) {
124 if (!isSystemIme(imi)) {
125 return false;
126 }
127 if (checkDefaultAttribute && !imi.isDefault(context)) {
128 return false;
129 }
130 if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
131 return false;
132 }
133 return true;
134 }
135
136 @Nullable
Yohei Yukawadc489242014-09-14 12:01:59 +0900137 public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis,
138 final Context context) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900139 // At first, find the fallback locale from the IMEs that are declared as "default" in the
140 // current locale. Note that IME developers can declare an IME as "default" only for
141 // some particular locales but "not default" for other locales.
Yohei Yukawadc489242014-09-14 12:01:59 +0900142 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
143 for (int i = 0; i < imis.size(); ++i) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900144 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
145 true /* checkDefaultAttribute */, fallbackLocale,
146 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900147 return fallbackLocale;
148 }
149 }
150 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900151 // If no fallback locale is found in the above condition, find fallback locales regardless
152 // of the "default" attribute as a last resort.
153 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
154 for (int i = 0; i < imis.size(); ++i) {
155 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
156 false /* checkDefaultAttribute */, fallbackLocale,
157 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
158 return fallbackLocale;
159 }
160 }
161 }
162 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
Yohei Yukawadc489242014-09-14 12:01:59 +0900163 return null;
164 }
165
Yohei Yukawab21220e2014-11-01 21:04:30 +0900166 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi,
167 final Context context, final boolean checkDefaultAttribute) {
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900168 if (!isSystemIme(imi)) {
169 return false;
170 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900171 if (checkDefaultAttribute && !imi.isDefault(context)) {
172 return false;
173 }
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900174 if (!imi.isAuxiliaryIme()) {
175 return false;
176 }
177 final int subtypeCount = imi.getSubtypeCount();
178 for (int i = 0; i < subtypeCount; ++i) {
179 final InputMethodSubtype s = imi.getSubtypeAt(i);
180 if (s.overridesImplicitlyEnabledSubtype()) {
181 return true;
182 }
183 }
184 return false;
185 }
186
Yohei Yukawadc489242014-09-14 12:01:59 +0900187 public static Locale getSystemLocaleFromContext(final Context context) {
188 try {
189 return context.getResources().getConfiguration().locale;
190 } catch (Resources.NotFoundException ex) {
191 return null;
192 }
193 }
194
Yohei Yukawab21220e2014-11-01 21:04:30 +0900195 private static final class InputMethodListBuilder {
196 // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
197 // order can have non-trivial effect in the call sites.
198 @NonNull
199 private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
Yohei Yukawadc489242014-09-14 12:01:59 +0900200
Yohei Yukawab21220e2014-11-01 21:04:30 +0900201 public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis,
202 final Context context, final boolean checkDefaultAttribute,
203 @Nullable final Locale locale, final boolean checkCountry,
204 final String requiredSubtypeMode) {
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900205 for (int i = 0; i < imis.size(); ++i) {
206 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900207 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
208 checkCountry, requiredSubtypeMode)) {
209 mInputMethodSet.add(imi);
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900210 }
211 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900212 return this;
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900213 }
214
Yohei Yukawab21220e2014-11-01 21:04:30 +0900215 // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
216 // documented more clearly.
217 public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis,
218 final Context context) {
219 // If one or more auxiliary input methods are available, OK to stop populating the list.
220 for (final InputMethodInfo imi : mInputMethodSet) {
221 if (imi.isAuxiliaryIme()) {
222 return this;
223 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900224 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900225 boolean added = false;
Yohei Yukawadc489242014-09-14 12:01:59 +0900226 for (int i = 0; i < imis.size(); ++i) {
227 final InputMethodInfo imi = imis.get(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900228 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
229 true /* checkDefaultAttribute */)) {
230 mInputMethodSet.add(imi);
231 added = true;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900232 }
233 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900234 if (added) {
235 return this;
236 }
237 for (int i = 0; i < imis.size(); ++i) {
238 final InputMethodInfo imi = imis.get(i);
239 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
240 false /* checkDefaultAttribute */)) {
241 mInputMethodSet.add(imi);
242 }
243 }
244 return this;
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900245 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900246
Yohei Yukawab21220e2014-11-01 21:04:30 +0900247 public boolean isEmpty() {
248 return mInputMethodSet.isEmpty();
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900249 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900250
251 @NonNull
252 public ArrayList<InputMethodInfo> build() {
253 return new ArrayList<>(mInputMethodSet);
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900254 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900255 }
256
Yohei Yukawab21220e2014-11-01 21:04:30 +0900257 private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale(
258 final ArrayList<InputMethodInfo> imis, final Context context,
259 @Nullable final Locale fallbackLocale) {
260 // Before the system becomes ready, we pick up at least one keyboard in the following order.
261 // The first user (device owner) falls into this category.
262 // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
263 // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
264 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
265 // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
266 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
267
268 final InputMethodListBuilder builder = new InputMethodListBuilder();
269 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
270 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
271 if (!builder.isEmpty()) {
272 return builder;
Yohei Yukawadc489242014-09-14 12:01:59 +0900273 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900274 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
275 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
276 if (!builder.isEmpty()) {
277 return builder;
278 }
279 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
280 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
281 if (!builder.isEmpty()) {
282 return builder;
283 }
284 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
285 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
286 if (!builder.isEmpty()) {
287 return builder;
288 }
289 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
290 + " fallbackLocale=" + fallbackLocale);
291 return builder;
292 }
293
294 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
295 final ArrayList<InputMethodInfo> imis, final Context context,
296 @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
297 // Once the system becomes ready, we pick up at least one keyboard in the following order.
298 // Secondary users fall into this category in general.
299 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
300 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
301 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
302 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
303 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
304 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
305 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
306
307 final InputMethodListBuilder builder = new InputMethodListBuilder();
308 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
309 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
310 if (!builder.isEmpty()) {
311 return builder;
312 }
313 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
314 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
315 if (!builder.isEmpty()) {
316 return builder;
317 }
318 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
319 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
320 if (!builder.isEmpty()) {
321 return builder;
322 }
323 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
324 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
325 if (!builder.isEmpty()) {
326 return builder;
327 }
328 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
329 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
330 if (!builder.isEmpty()) {
331 return builder;
332 }
333 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
334 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
335 if (!builder.isEmpty()) {
336 return builder;
337 }
338 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
339 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
340 return builder;
341 }
342
343 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
344 final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) {
345 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
346 if (!isSystemReady) {
347 // When the system is not ready, the system locale is not stable and reliable. Hence
348 // we will pick up IMEs that support software keyboard based on the fallback locale.
349 // Also pick up suitable IMEs regardless of the software keyboard support.
350 // (e.g. Voice IMEs)
351 return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale)
352 .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
353 true /* checkCountry */, SUBTYPE_MODE_ANY)
354 .build();
355 }
356
357 // When the system is ready, we will primarily rely on the system locale, but also keep
358 // relying on the fallback locale as a last resort.
359 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
360 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
361 // subtype)
362 final Locale systemLocale = getSystemLocaleFromContext(context);
363 return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
364 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
365 true /* checkCountry */, SUBTYPE_MODE_ANY)
366 .fillAuxiliaryImes(imis, context)
367 .build();
Yohei Yukawadc489242014-09-14 12:01:59 +0900368 }
369
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900370 public static Locale constructLocaleFromString(String localeStr) {
371 if (TextUtils.isEmpty(localeStr)) {
372 return null;
373 }
374 // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
375 String[] localeParams = localeStr.split("_", 3);
Yohei Yukawaed65bc02015-12-02 18:22:41 -0800376 if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
377 // Convert a locale whose language is "tl" to one whose language is "fil".
378 // For example, "tl_PH" will get converted to "fil_PH".
379 // Versions of Android earlier than Lollipop did not support three letter language
380 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
381 // On Lollipop and above, the current three letter version must be used.
382 localeParams[0] = "fil";
383 }
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900384 // The length of localeStr is guaranteed to always return a 1 <= value <= 3
385 // because localeStr is not empty.
386 if (localeParams.length == 1) {
387 return new Locale(localeParams[0]);
388 } else if (localeParams.length == 2) {
389 return new Locale(localeParams[0], localeParams[1]);
390 } else if (localeParams.length == 3) {
391 return new Locale(localeParams[0], localeParams[1], localeParams[2]);
392 }
393 return null;
394 }
395
Yohei Yukawadc489242014-09-14 12:01:59 +0900396 public static boolean containsSubtypeOf(final InputMethodInfo imi,
Yohei Yukawab21220e2014-11-01 21:04:30 +0900397 @Nullable final Locale locale, final boolean checkCountry, final String mode) {
398 if (locale == null) {
399 return false;
400 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900401 final int N = imi.getSubtypeCount();
402 for (int i = 0; i < N; ++i) {
403 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
Yohei Yukawab21220e2014-11-01 21:04:30 +0900404 if (checkCountry) {
Yohei Yukawa92280cd2015-06-02 16:50:14 -0700405 final Locale subtypeLocale = subtype.getLocaleObject();
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900406 if (subtypeLocale == null ||
407 !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
408 !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
Yohei Yukawadc489242014-09-14 12:01:59 +0900409 continue;
410 }
Yohei Yukawab21220e2014-11-01 21:04:30 +0900411 } else {
412 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
413 subtype.getLocale()));
Yohei Yukawaf487e0e2015-02-21 02:15:48 +0900414 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
Yohei Yukawab21220e2014-11-01 21:04:30 +0900415 continue;
416 }
Yohei Yukawadc489242014-09-14 12:01:59 +0900417 }
418 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
419 mode.equalsIgnoreCase(subtype.getMode())) {
420 return true;
421 }
422 }
423 return false;
424 }
425
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900426 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700427 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900428 final int subtypeCount = imi.getSubtypeCount();
429 for (int i = 0; i < subtypeCount; ++i) {
430 subtypes.add(imi.getSubtypeAt(i));
431 }
432 return subtypes;
433 }
434
435 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
436 InputMethodInfo imi, String mode) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700437 ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900438 final int subtypeCount = imi.getSubtypeCount();
439 for (int i = 0; i < subtypeCount; ++i) {
440 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
441 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
442 subtypes.add(subtype);
443 }
444 }
445 return subtypes;
446 }
447
Yohei Yukawa68c860b2014-09-13 22:03:37 +0900448 public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900449 if (enabledImes == null || enabledImes.isEmpty()) {
450 return null;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900451 }
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900452 // We'd prefer to fall back on a system IME, since that is safer.
453 int i = enabledImes.size();
454 int firstFoundSystemIme = -1;
455 while (i > 0) {
456 i--;
457 final InputMethodInfo imi = enabledImes.get(i);
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900458 if (imi.isAuxiliaryIme()) {
459 continue;
460 }
461 if (InputMethodUtils.isSystemIme(imi)
462 && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
463 SUBTYPE_MODE_KEYBOARD)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900464 return imi;
465 }
Yohei Yukawa6aa03782015-02-21 03:00:22 +0900466 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
Yohei Yukawa5e5c60a2014-09-13 01:13:38 +0900467 firstFoundSystemIme = i;
468 }
469 }
470 return enabledImes.get(Math.max(firstFoundSystemIme, 0));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900471 }
472
473 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
474 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
475 }
476
477 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
478 if (imi != null) {
479 final int subtypeCount = imi.getSubtypeCount();
480 for (int i = 0; i < subtypeCount; ++i) {
481 InputMethodSubtype ims = imi.getSubtypeAt(i);
482 if (subtypeHashCode == ims.hashCode()) {
483 return i;
484 }
485 }
486 }
487 return NOT_A_SUBTYPE_ID;
488 }
489
Yohei Yukawae985c242016-02-24 18:27:04 -0800490 private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
491 new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
492 @Override
493 public Locale get(InputMethodSubtype source) {
494 return source != null ? source.getLocaleObject() : null;
495 }
496 };
497
Yohei Yukawae72d1c82015-02-20 20:55:21 +0900498 @VisibleForTesting
499 public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900500 Resources res, InputMethodInfo imi) {
501 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
Yohei Yukawae985c242016-02-24 18:27:04 -0800502 final LocaleList systemLocales = res.getConfiguration().getLocales();
503 final String systemLocale = systemLocales.get(0).toString();
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700504 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800505 final int numSubtypes = subtypes.size();
506
507 // Handle overridesImplicitlyEnabledSubtype mechanism.
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700508 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800509 for (int i = 0; i < numSubtypes; ++i) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900510 // scan overriding implicitly enabled subtypes.
Yohei Yukawae985c242016-02-24 18:27:04 -0800511 final InputMethodSubtype subtype = subtypes.get(i);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900512 if (subtype.overridesImplicitlyEnabledSubtype()) {
513 final String mode = subtype.getMode();
514 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
515 applicableModeAndSubtypesMap.put(mode, subtype);
516 }
517 }
518 }
519 if (applicableModeAndSubtypesMap.size() > 0) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700520 return new ArrayList<>(applicableModeAndSubtypesMap.values());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900521 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800522
Yohei Yukawa238faad2016-03-11 10:39:51 -0800523 final HashMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
524 new HashMap<>();
Yohei Yukawae985c242016-02-24 18:27:04 -0800525 final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
Yohei Yukawa238faad2016-03-11 10:39:51 -0800526
Yohei Yukawae985c242016-02-24 18:27:04 -0800527 for (int i = 0; i < numSubtypes; ++i) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900528 final InputMethodSubtype subtype = subtypes.get(i);
Yohei Yukawa238faad2016-03-11 10:39:51 -0800529 final String mode = subtype.getMode();
530 if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
Yohei Yukawae985c242016-02-24 18:27:04 -0800531 keyboardSubtypes.add(subtype);
532 } else {
Yohei Yukawa238faad2016-03-11 10:39:51 -0800533 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
534 nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900535 }
Yohei Yukawa238faad2016-03-11 10:39:51 -0800536 nonKeyboardSubtypesMap.get(mode).add(subtype);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900537 }
538 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800539
540 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
541 LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
542 applicableSubtypes);
543
Yohei Yukawa42275bc2016-03-03 00:34:27 -0800544 if (!applicableSubtypes.isEmpty()) {
545 boolean hasAsciiCapableKeyboard = false;
546 final int numApplicationSubtypes = applicableSubtypes.size();
547 for (int i = 0; i < numApplicationSubtypes; ++i) {
548 final InputMethodSubtype subtype = applicableSubtypes.get(i);
549 if (subtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
550 hasAsciiCapableKeyboard = true;
551 break;
552 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800553 }
Yohei Yukawa42275bc2016-03-03 00:34:27 -0800554 if (!hasAsciiCapableKeyboard) {
555 final int numKeyboardSubtypes = keyboardSubtypes.size();
556 for (int i = 0; i < numKeyboardSubtypes; ++i) {
557 final InputMethodSubtype subtype = keyboardSubtypes.get(i);
558 final String mode = subtype.getMode();
559 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
560 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
561 applicableSubtypes.add(subtype);
562 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900563 }
564 }
565 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800566
567 if (applicableSubtypes.isEmpty()) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900568 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
569 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
570 if (lastResortKeyboardSubtype != null) {
571 applicableSubtypes.add(lastResortKeyboardSubtype);
572 }
573 }
Yohei Yukawae985c242016-02-24 18:27:04 -0800574
Yohei Yukawa238faad2016-03-11 10:39:51 -0800575 // For each non-keyboard mode, extract subtypes with system locales.
576 for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
577 LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
578 applicableSubtypes);
579 }
580
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900581 return applicableSubtypes;
582 }
583
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900584 /**
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100585 * Returns the language component of a given locale string.
Yohei Yukawab21220e2014-11-01 21:04:30 +0900586 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100587 */
Tadashi G. Takaoka77cbcb62014-07-12 16:08:20 +0900588 public static String getLanguageFromLocaleString(String locale) {
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100589 final int idx = locale.indexOf('_');
590 if (idx < 0) {
591 return locale;
592 } else {
593 return locale.substring(0, idx);
594 }
595 }
596
597 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900598 * If there are no selected subtypes, tries finding the most applicable one according to the
599 * given locale.
600 * @param subtypes this function will search the most applicable subtype in subtypes
601 * @param mode subtypes will be filtered by mode
602 * @param locale subtypes will be filtered by locale
603 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
604 * it will return the first subtype matched with mode
605 * @return the most applicable subtypeId
606 */
607 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
608 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
609 boolean canIgnoreLocaleAsLastResort) {
610 if (subtypes == null || subtypes.size() == 0) {
611 return null;
612 }
613 if (TextUtils.isEmpty(locale)) {
614 locale = res.getConfiguration().locale.toString();
615 }
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100616 final String language = getLanguageFromLocaleString(locale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900617 boolean partialMatchFound = false;
618 InputMethodSubtype applicableSubtype = null;
619 InputMethodSubtype firstMatchedModeSubtype = null;
620 final int N = subtypes.size();
621 for (int i = 0; i < N; ++i) {
622 InputMethodSubtype subtype = subtypes.get(i);
623 final String subtypeLocale = subtype.getLocale();
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100624 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900625 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
626 // and all subtypes with all modes can be candidates.
627 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
628 if (firstMatchedModeSubtype == null) {
629 firstMatchedModeSubtype = subtype;
630 }
631 if (locale.equals(subtypeLocale)) {
632 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
633 applicableSubtype = subtype;
634 break;
Narayan Kamath4d8c1322014-07-11 11:50:24 +0100635 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900636 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
637 applicableSubtype = subtype;
638 partialMatchFound = true;
639 }
640 }
641 }
642
643 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
644 return firstMatchedModeSubtype;
645 }
646
647 // The first subtype applicable to the system locale will be defined as the most applicable
648 // subtype.
649 if (DEBUG) {
650 if (applicableSubtype != null) {
651 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
652 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
653 }
654 }
655 return applicableSubtype;
656 }
657
658 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
659 if (subtype == null) return true;
660 return !subtype.isAuxiliary();
661 }
662
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900663 public static void setNonSelectedSystemImesDisabledUntilUsed(
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700664 IPackageManager packageManager, List<InputMethodInfo> enabledImis,
665 int userId, String callingPackage) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900666 if (DEBUG) {
667 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
668 }
669 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
670 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
671 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
672 return;
673 }
674 // Only the current spell checker should be treated as an enabled one.
675 final SpellCheckerInfo currentSpellChecker =
676 TextServicesManager.getInstance().getCurrentSpellChecker();
677 for (final String packageName : systemImesDisabledUntilUsed) {
678 if (DEBUG) {
679 Slog.d(TAG, "check " + packageName);
680 }
681 boolean enabledIme = false;
682 for (int j = 0; j < enabledImis.size(); ++j) {
683 final InputMethodInfo imi = enabledImis.get(j);
684 if (packageName.equals(imi.getPackageName())) {
685 enabledIme = true;
686 break;
687 }
688 }
689 if (enabledIme) {
690 // enabled ime. skip
691 continue;
692 }
693 if (currentSpellChecker != null
694 && packageName.equals(currentSpellChecker.getPackageName())) {
695 // enabled spell checker. skip
696 if (DEBUG) {
697 Slog.d(TAG, packageName + " is the current spell checker. skip");
698 }
699 continue;
700 }
701 ApplicationInfo ai = null;
702 try {
703 ai = packageManager.getApplicationInfo(packageName,
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700704 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
705 } catch (RemoteException e) {
706 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
707 + " userId=" + userId, e);
708 continue;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900709 }
710 if (ai == null) {
711 // No app found for packageName
712 continue;
713 }
714 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
715 if (!isSystemPackage) {
716 continue;
717 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700718 setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900719 }
720 }
721
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700722 private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
723 int userId, String callingPackage) {
724 final int state;
725 try {
726 state = packageManager.getApplicationEnabledSetting(packageName, userId);
727 } catch (RemoteException e) {
728 Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
729 + " userId=" + userId, e);
730 return;
731 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900732 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
733 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
734 if (DEBUG) {
735 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
736 }
Yohei Yukawa094c71f2015-06-20 00:41:31 -0700737 try {
738 packageManager.setApplicationEnabledSetting(packageName,
739 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
740 0 /* newState */, userId, callingPackage);
741 } catch (RemoteException e) {
742 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
743 + " userId=" + userId + " callingPackage=" + callingPackage, e);
744 return;
745 }
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900746 } else {
747 if (DEBUG) {
748 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
749 }
750 }
751 }
752
Satoshi Kataokab2827262013-07-04 19:43:14 +0900753 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
754 InputMethodSubtype subtype) {
755 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
756 return subtype != null
757 ? TextUtils.concat(subtype.getDisplayName(context,
758 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
759 (TextUtils.isEmpty(imiLabel) ?
760 "" : " - " + imiLabel))
761 : imiLabel;
762 }
763
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900764 /**
Yohei Yukawae63b5fa2014-09-19 13:14:55 +0900765 * Returns true if a package name belongs to a UID.
766 *
767 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
768 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
769 * @param uid the UID to be validated.
770 * @param packageName the package name.
771 * @return {@code true} if the package name belongs to the UID.
772 */
773 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
774 final int uid, final String packageName) {
775 try {
776 appOpsManager.checkPackage(uid, packageName);
777 return true;
778 } catch (SecurityException e) {
779 return false;
780 }
781 }
782
783 /**
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900784 * Parses the setting stored input methods and subtypes string value.
785 *
786 * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
787 * @return Map from input method ID to set of input method subtypes IDs.
788 */
789 @VisibleForTesting
790 public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
791 @Nullable final String inputMethodsAndSubtypesString) {
792
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800793 final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900794 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
795 return imeMap;
796 }
797
798 final SimpleStringSplitter typeSplitter =
799 new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
800 final SimpleStringSplitter subtypeSplitter =
801 new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
802
803 List<Pair<String, ArrayList<String>>> allImeSettings =
804 InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
805 typeSplitter,
806 subtypeSplitter);
807 for (Pair<String, ArrayList<String>> ime : allImeSettings) {
Yohei Yukawa622b44d2016-02-11 07:56:53 -0800808 ArraySet<String> subtypes = new ArraySet<>();
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900809 if (ime.second != null) {
810 subtypes.addAll(ime.second);
811 }
812 imeMap.put(ime.first, subtypes);
813 }
814 return imeMap;
815 }
816
Seigo Nonaka2a099bc2015-08-14 19:29:45 -0700817 @NonNull
818 public static String buildInputMethodsAndSubtypesString(
819 @NonNull final ArrayMap<String, ArraySet<String>> map) {
820 // we want to use the canonical InputMethodSettings implementation,
821 // so we convert data structures first.
822 List<Pair<String, ArrayList<String>>> imeMap = new ArrayList<>(4);
823 for (ArrayMap.Entry<String, ArraySet<String>> entry : map.entrySet()) {
824 final String imeName = entry.getKey();
825 final ArraySet<String> subtypeSet = entry.getValue();
826 final ArrayList<String> subtypes = new ArrayList<>(2);
827 if (subtypeSet != null) {
828 subtypes.addAll(subtypeSet);
829 }
830 imeMap.add(new Pair<>(imeName, subtypes));
831 }
832 return InputMethodSettings.buildInputMethodsSettingString(imeMap);
833 }
834
Seigo Nonaka2028dda2015-07-06 17:41:24 +0900835 /**
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900836 * Utility class for putting and getting settings for InputMethod
837 * TODO: Move all putters and getters of settings to this class.
838 */
839 public static class InputMethodSettings {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900840 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700841 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900842
843 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
Seigo Nonakace2c7842015-08-17 08:47:36 -0700844 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900845
846 private final Resources mRes;
847 private final ContentResolver mResolver;
848 private final HashMap<String, InputMethodInfo> mMethodMap;
849 private final ArrayList<InputMethodInfo> mMethodList;
850
Yohei Yukawa68645a62016-02-17 07:54:20 -0800851 /**
852 * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
853 */
854 private final HashMap<String, String> mCopyOnWriteDataStore = new HashMap<>();
855
856 private boolean mCopyOnWrite = false;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900857 private String mEnabledInputMethodsStrCache;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800858 @UserIdInt
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900859 private int mCurrentUserId;
Kenny Guy2a764942014-04-02 13:29:20 +0100860 private int[] mCurrentProfileIds = new int[0];
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900861
862 private static void buildEnabledInputMethodsSettingString(
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700863 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
864 builder.append(ime.first);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900865 // Inputmethod and subtypes are saved in the settings as follows:
866 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700867 for (String subtypeId: ime.second) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700868 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900869 }
870 }
871
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700872 public static String buildInputMethodsSettingString(
873 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
874 final StringBuilder b = new StringBuilder();
875 boolean needsSeparator = false;
876 for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
877 if (needsSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -0700878 b.append(INPUT_METHOD_SEPARATOR);
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700879 }
880 buildEnabledInputMethodsSettingString(b, ime);
881 needsSeparator = true;
882 }
883 return b.toString();
884 }
885
886 public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
887 String enabledInputMethodsStr,
888 TextUtils.SimpleStringSplitter inputMethodSplitter,
889 TextUtils.SimpleStringSplitter subtypeSplitter) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700890 ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700891 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
892 return imsList;
893 }
894 inputMethodSplitter.setString(enabledInputMethodsStr);
895 while (inputMethodSplitter.hasNext()) {
896 String nextImsStr = inputMethodSplitter.next();
897 subtypeSplitter.setString(nextImsStr);
898 if (subtypeSplitter.hasNext()) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700899 ArrayList<String> subtypeHashes = new ArrayList<>();
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700900 // The first element is ime id.
901 String imeId = subtypeSplitter.next();
902 while (subtypeSplitter.hasNext()) {
903 subtypeHashes.add(subtypeSplitter.next());
904 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -0700905 imsList.add(new Pair<>(imeId, subtypeHashes));
Christopher Tate7b9a28c2015-03-18 13:06:16 -0700906 }
907 }
908 return imsList;
909 }
910
Yohei Yukawa68645a62016-02-17 07:54:20 -0800911 @Deprecated
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900912 public InputMethodSettings(
913 Resources res, ContentResolver resolver,
914 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
Yohei Yukawa68645a62016-02-17 07:54:20 -0800915 @UserIdInt int userId) {
916 this(res, resolver, methodMap, methodList, userId, false /* copyOnWrite */);
917 }
918
919 public InputMethodSettings(
920 Resources res, ContentResolver resolver,
921 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
922 @UserIdInt int userId, boolean copyOnWrite) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900923 mRes = res;
924 mResolver = resolver;
925 mMethodMap = methodMap;
926 mMethodList = methodList;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800927 switchCurrentUser(userId, copyOnWrite);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900928 }
929
Yohei Yukawa68645a62016-02-17 07:54:20 -0800930 /**
931 * Must be called when the current user is changed.
932 *
933 * @param userId The user ID.
934 * @param copyOnWrite If {@code true}, for each settings key
935 * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
936 * settings on the {@link Settings.Secure} until we do the first write operation.
937 */
938 public void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900939 if (DEBUG) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800940 Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900941 }
Yohei Yukawa68645a62016-02-17 07:54:20 -0800942 if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
943 mCopyOnWriteDataStore.clear();
944 mEnabledInputMethodsStrCache = "";
945 // TODO: mCurrentProfileIds should be cleared here.
946 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900947 mCurrentUserId = userId;
Yohei Yukawa68645a62016-02-17 07:54:20 -0800948 mCopyOnWrite = copyOnWrite;
949 // TODO: mCurrentProfileIds should be updated here.
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900950 }
951
Yohei Yukawa87523672016-02-12 19:37:08 -0800952 private void putString(final String key, final String str) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800953 if (mCopyOnWrite) {
954 mCopyOnWriteDataStore.put(key, str);
955 } else {
956 Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId);
957 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800958 }
959
960 private String getString(final String key) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800961 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
962 final String result = mCopyOnWriteDataStore.get(key);
963 return result != null ? result : "";
964 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800965 return Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
966 }
967
968 private void putInt(final String key, final int value) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800969 if (mCopyOnWrite) {
970 mCopyOnWriteDataStore.put(key, String.valueOf(value));
971 } else {
972 Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId);
973 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800974 }
975
976 private int getInt(final String key, final int defaultValue) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800977 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
978 final String result = mCopyOnWriteDataStore.get(key);
979 return result != null ? Integer.valueOf(result) : 0;
980 }
Yohei Yukawa87523672016-02-12 19:37:08 -0800981 return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
982 }
983
984 private void putBoolean(final String key, final boolean value) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800985 putInt(key, value ? 1 : 0);
Yohei Yukawa87523672016-02-12 19:37:08 -0800986 }
987
988 private boolean getBoolean(final String key, final boolean defaultValue) {
Yohei Yukawa68645a62016-02-17 07:54:20 -0800989 return getInt(key, defaultValue ? 1 : 0) == 1;
Yohei Yukawa87523672016-02-12 19:37:08 -0800990 }
991
Kenny Guy2a764942014-04-02 13:29:20 +0100992 public void setCurrentProfileIds(int[] currentProfileIds) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800993 synchronized (this) {
Kenny Guy2a764942014-04-02 13:29:20 +0100994 mCurrentProfileIds = currentProfileIds;
Amith Yamasani734983f2014-03-04 16:48:05 -0800995 }
996 }
997
Kenny Guy2a764942014-04-02 13:29:20 +0100998 public boolean isCurrentProfile(int userId) {
Amith Yamasani734983f2014-03-04 16:48:05 -0800999 synchronized (this) {
Kenny Guyf4824a02014-04-02 19:17:41 +01001000 if (userId == mCurrentUserId) return true;
Kenny Guy2a764942014-04-02 13:29:20 +01001001 for (int i = 0; i < mCurrentProfileIds.length; i++) {
1002 if (userId == mCurrentProfileIds[i]) return true;
Amith Yamasani734983f2014-03-04 16:48:05 -08001003 }
1004 return false;
1005 }
1006 }
1007
Yohei Yukawac2393ac2016-02-18 00:30:45 -08001008 public ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001009 return createEnabledInputMethodListLocked(
1010 getEnabledInputMethodsAndSubtypeListLocked());
1011 }
1012
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001013 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
1014 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
1015 List<InputMethodSubtype> enabledSubtypes =
1016 getEnabledInputMethodSubtypeListLocked(imi);
1017 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
1018 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
1019 context.getResources(), imi);
1020 }
1021 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
1022 }
1023
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +09001024 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001025 InputMethodInfo imi) {
1026 List<Pair<String, ArrayList<String>>> imsList =
1027 getEnabledInputMethodsAndSubtypeListLocked();
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001028 ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001029 if (imi != null) {
1030 for (Pair<String, ArrayList<String>> imsPair : imsList) {
1031 InputMethodInfo info = mMethodMap.get(imsPair.first);
1032 if (info != null && info.getId().equals(imi.getId())) {
1033 final int subtypeCount = info.getSubtypeCount();
1034 for (int i = 0; i < subtypeCount; ++i) {
1035 InputMethodSubtype ims = info.getSubtypeAt(i);
1036 for (String s: imsPair.second) {
1037 if (String.valueOf(ims.hashCode()).equals(s)) {
1038 enabledSubtypes.add(ims);
1039 }
1040 }
1041 }
1042 break;
1043 }
1044 }
1045 }
1046 return enabledSubtypes;
1047 }
1048
1049 // At the initial boot, the settings for input methods are not set,
1050 // so we need to enable IME in that case.
1051 public void enableAllIMEsIfThereIsNoEnabledIME() {
1052 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
1053 StringBuilder sb = new StringBuilder();
1054 final int N = mMethodList.size();
1055 for (int i = 0; i < N; i++) {
1056 InputMethodInfo imi = mMethodList.get(i);
1057 Slog.i(TAG, "Adding: " + imi.getId());
1058 if (i > 0) sb.append(':');
1059 sb.append(imi.getId());
1060 }
1061 putEnabledInputMethodsStr(sb.toString());
1062 }
1063 }
1064
1065 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
Christopher Tate7b9a28c2015-03-18 13:06:16 -07001066 return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
1067 mInputMethodSplitter,
1068 mSubtypeSplitter);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001069 }
1070
1071 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
1072 if (reloadInputMethodStr) {
1073 getEnabledInputMethodsStr();
1074 }
1075 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
1076 // Add in the newly enabled input method.
1077 putEnabledInputMethodsStr(id);
1078 } else {
1079 putEnabledInputMethodsStr(
Seigo Nonakace2c7842015-08-17 08:47:36 -07001080 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001081 }
1082 }
1083
1084 /**
1085 * Build and put a string of EnabledInputMethods with removing specified Id.
1086 * @return the specified id was removed or not.
1087 */
1088 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
1089 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
1090 boolean isRemoved = false;
1091 boolean needsAppendSeparator = false;
1092 for (Pair<String, ArrayList<String>> ims: imsList) {
1093 String curId = ims.first;
1094 if (curId.equals(id)) {
1095 // We are disabling this input method, and it is
1096 // currently enabled. Skip it to remove from the
1097 // new list.
1098 isRemoved = true;
1099 } else {
1100 if (needsAppendSeparator) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001101 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001102 } else {
1103 needsAppendSeparator = true;
1104 }
1105 buildEnabledInputMethodsSettingString(builder, ims);
1106 }
1107 }
1108 if (isRemoved) {
1109 // Update the setting with the new list of input methods.
1110 putEnabledInputMethodsStr(builder.toString());
1111 }
1112 return isRemoved;
1113 }
1114
Yohei Yukawac2393ac2016-02-18 00:30:45 -08001115 private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001116 List<Pair<String, ArrayList<String>>> imsList) {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001117 final ArrayList<InputMethodInfo> res = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001118 for (Pair<String, ArrayList<String>> ims: imsList) {
1119 InputMethodInfo info = mMethodMap.get(ims.first);
1120 if (info != null) {
1121 res.add(info);
1122 }
1123 }
1124 return res;
1125 }
1126
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001127 private void putEnabledInputMethodsStr(String str) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001128 if (DEBUG) {
1129 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1130 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001131 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
1132 mEnabledInputMethodsStrCache = str;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001133 }
1134
Dianne Hackbornfd7aded2013-01-22 17:10:23 -08001135 public String getEnabledInputMethodsStr() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001136 mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001137 if (DEBUG) {
1138 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1139 + ", " + mCurrentUserId);
1140 }
1141 return mEnabledInputMethodsStrCache;
1142 }
1143
1144 private void saveSubtypeHistory(
1145 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1146 StringBuilder builder = new StringBuilder();
1147 boolean isImeAdded = false;
1148 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001149 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001150 newSubtypeId);
1151 isImeAdded = true;
1152 }
1153 for (Pair<String, String> ime: savedImes) {
1154 String imeId = ime.first;
1155 String subtypeId = ime.second;
1156 if (TextUtils.isEmpty(subtypeId)) {
1157 subtypeId = NOT_A_SUBTYPE_ID_STR;
1158 }
1159 if (isImeAdded) {
Seigo Nonakace2c7842015-08-17 08:47:36 -07001160 builder.append(INPUT_METHOD_SEPARATOR);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001161 } else {
1162 isImeAdded = true;
1163 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001164 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001165 subtypeId);
1166 }
Seigo Nonakace2c7842015-08-17 08:47:36 -07001167 // Remove the last INPUT_METHOD_SEPARATOR
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001168 putSubtypeHistoryStr(builder.toString());
1169 }
1170
1171 private void addSubtypeToHistory(String imeId, String subtypeId) {
1172 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1173 for (Pair<String, String> ime: subtypeHistory) {
1174 if (ime.first.equals(imeId)) {
1175 if (DEBUG) {
1176 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1177 + ime.second);
1178 }
1179 // We should break here
1180 subtypeHistory.remove(ime);
1181 break;
1182 }
1183 }
1184 if (DEBUG) {
1185 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1186 }
1187 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1188 }
1189
1190 private void putSubtypeHistoryStr(String str) {
1191 if (DEBUG) {
1192 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1193 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001194 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001195 }
1196
1197 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1198 // Gets the first one from the history
1199 return getLastSubtypeForInputMethodLockedInternal(null);
1200 }
1201
1202 public String getLastSubtypeForInputMethodLocked(String imeId) {
1203 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1204 if (ime != null) {
1205 return ime.second;
1206 } else {
1207 return null;
1208 }
1209 }
1210
1211 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1212 List<Pair<String, ArrayList<String>>> enabledImes =
1213 getEnabledInputMethodsAndSubtypeListLocked();
1214 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1215 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1216 final String imeInTheHistory = imeAndSubtype.first;
1217 // If imeId is empty, returns the first IME and subtype in the history
1218 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1219 final String subtypeInTheHistory = imeAndSubtype.second;
1220 final String subtypeHashCode =
1221 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1222 enabledImes, imeInTheHistory, subtypeInTheHistory);
1223 if (!TextUtils.isEmpty(subtypeHashCode)) {
1224 if (DEBUG) {
1225 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1226 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001227 return new Pair<>(imeInTheHistory, subtypeHashCode);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001228 }
1229 }
1230 }
1231 if (DEBUG) {
1232 Slog.d(TAG, "No enabled IME found in the history");
1233 }
1234 return null;
1235 }
1236
1237 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1238 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1239 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1240 if (enabledIme.first.equals(imeId)) {
1241 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1242 final InputMethodInfo imi = mMethodMap.get(imeId);
1243 if (explicitlyEnabledSubtypes.size() == 0) {
1244 // If there are no explicitly enabled subtypes, applicable subtypes are
1245 // enabled implicitly.
1246 // If IME is enabled and no subtypes are enabled, applicable subtypes
1247 // are enabled implicitly, so needs to treat them to be enabled.
1248 if (imi != null && imi.getSubtypeCount() > 0) {
1249 List<InputMethodSubtype> implicitlySelectedSubtypes =
1250 getImplicitlyApplicableSubtypesLocked(mRes, imi);
1251 if (implicitlySelectedSubtypes != null) {
1252 final int N = implicitlySelectedSubtypes.size();
1253 for (int i = 0; i < N; ++i) {
1254 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1255 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1256 return subtypeHashCode;
1257 }
1258 }
1259 }
1260 }
1261 } else {
1262 for (String s: explicitlyEnabledSubtypes) {
1263 if (s.equals(subtypeHashCode)) {
1264 // If both imeId and subtypeId are enabled, return subtypeId.
1265 try {
1266 final int hashCode = Integer.valueOf(subtypeHashCode);
1267 // Check whether the subtype id is valid or not
1268 if (isValidSubtypeId(imi, hashCode)) {
1269 return s;
1270 } else {
1271 return NOT_A_SUBTYPE_ID_STR;
1272 }
1273 } catch (NumberFormatException e) {
1274 return NOT_A_SUBTYPE_ID_STR;
1275 }
1276 }
1277 }
1278 }
1279 // If imeId was enabled but subtypeId was disabled.
1280 return NOT_A_SUBTYPE_ID_STR;
1281 }
1282 }
1283 // If both imeId and subtypeId are disabled, return null
1284 return null;
1285 }
1286
1287 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001288 ArrayList<Pair<String, String>> imsList = new ArrayList<>();
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001289 final String subtypeHistoryStr = getSubtypeHistoryStr();
1290 if (TextUtils.isEmpty(subtypeHistoryStr)) {
1291 return imsList;
1292 }
1293 mInputMethodSplitter.setString(subtypeHistoryStr);
1294 while (mInputMethodSplitter.hasNext()) {
1295 String nextImsStr = mInputMethodSplitter.next();
1296 mSubtypeSplitter.setString(nextImsStr);
1297 if (mSubtypeSplitter.hasNext()) {
1298 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1299 // The first element is ime id.
1300 String imeId = mSubtypeSplitter.next();
1301 while (mSubtypeSplitter.hasNext()) {
1302 subtypeId = mSubtypeSplitter.next();
1303 break;
1304 }
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001305 imsList.add(new Pair<>(imeId, subtypeId));
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001306 }
1307 }
1308 return imsList;
1309 }
1310
1311 private String getSubtypeHistoryStr() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001312 final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001313 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001314 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001315 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001316 return history;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001317 }
1318
1319 public void putSelectedInputMethod(String imeId) {
1320 if (DEBUG) {
1321 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1322 + mCurrentUserId);
1323 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001324 putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001325 }
1326
1327 public void putSelectedSubtype(int subtypeId) {
1328 if (DEBUG) {
1329 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1330 + mCurrentUserId);
1331 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001332 putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001333 }
1334
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001335 public String getSelectedInputMethod() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001336 final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001337 if (DEBUG) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001338 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001339 }
Yohei Yukawa87523672016-02-12 19:37:08 -08001340 return imi;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001341 }
1342
1343 public boolean isSubtypeSelected() {
1344 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1345 }
1346
1347 private int getSelectedInputMethodSubtypeHashCode() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001348 return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001349 }
1350
Michael Wright7b5a96b2014-08-09 19:28:42 -07001351 public boolean isShowImeWithHardKeyboardEnabled() {
Yohei Yukawa87523672016-02-12 19:37:08 -08001352 return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001353 }
1354
1355 public void setShowImeWithHardKeyboard(boolean show) {
Yohei Yukawa87523672016-02-12 19:37:08 -08001356 putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
Michael Wright7b5a96b2014-08-09 19:28:42 -07001357 }
1358
Yohei Yukawa68645a62016-02-17 07:54:20 -08001359 @UserIdInt
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001360 public int getCurrentUserId() {
1361 return mCurrentUserId;
1362 }
1363
1364 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
1365 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1366 if (imi == null) {
1367 return NOT_A_SUBTYPE_ID;
1368 }
1369 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1370 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1371 }
1372
1373 public void saveCurrentInputMethodAndSubtypeToHistory(
1374 String curMethodId, InputMethodSubtype currentSubtype) {
1375 String subtypeId = NOT_A_SUBTYPE_ID_STR;
1376 if (currentSubtype != null) {
1377 subtypeId = String.valueOf(currentSubtype.hashCode());
1378 }
1379 if (canAddToLastInputMethod(currentSubtype)) {
1380 addSubtypeToHistory(curMethodId, subtypeId);
1381 }
1382 }
Satoshi Kataokad787f692013-10-26 04:44:21 +09001383
1384 public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1385 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1386 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
Yohei Yukawab0377bb2015-08-10 21:06:30 -07001387 new HashMap<>();
Satoshi Kataokad787f692013-10-26 04:44:21 +09001388 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1389 enabledInputMethodAndSubtypes.put(
1390 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1391 }
1392 return enabledInputMethodAndSubtypes;
1393 }
Yohei Yukawa68645a62016-02-17 07:54:20 -08001394
1395 public void dumpLocked(final Printer pw, final String prefix) {
1396 pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
1397 pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
1398 pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
1399 pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
1400 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001401 }
Yohei Yukawa174843a2015-06-26 18:02:54 -07001402
1403 // For spell checker service manager.
1404 // TODO: Should we have TextServicesUtils.java?
1405 private static final Locale LOCALE_EN_US = new Locale("en", "US");
1406 private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
1407
1408 /**
1409 * Returns a list of {@link Locale} in the order of appropriateness for the default spell
1410 * checker service.
1411 *
1412 * <p>If the system language is English, and the region is also explicitly specified in the
1413 * system locale, the following fallback order will be applied.</p>
1414 * <ul>
1415 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1416 * <li>(system-locale-language, system-locale-region)</li>
1417 * <li>("en", "US")</li>
1418 * <li>("en", "GB")</li>
1419 * <li>("en")</li>
1420 * </ul>
1421 *
1422 * <p>If the system language is English, but no region is specified in the system locale,
1423 * the following fallback order will be applied.</p>
1424 * <ul>
1425 * <li>("en")</li>
1426 * <li>("en", "US")</li>
1427 * <li>("en", "GB")</li>
1428 * </ul>
1429 *
1430 * <p>If the system language is not English, the following fallback order will be applied.</p>
1431 * <ul>
1432 * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
1433 * <li>(system-locale-language, system-locale-region) (if exists)</li>
1434 * <li>(system-locale-language) (if exists)</li>
1435 * <li>("en", "US")</li>
1436 * <li>("en", "GB")</li>
1437 * <li>("en")</li>
1438 * </ul>
1439 *
1440 * @param systemLocale the current system locale to be taken into consideration.
1441 * @return a list of {@link Locale}. The first one is considered to be most appropriate.
1442 */
1443 @VisibleForTesting
1444 public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
1445 @Nullable final Locale systemLocale) {
1446 final Locale systemLocaleLanguageCountryVariant;
1447 final Locale systemLocaleLanguageCountry;
1448 final Locale systemLocaleLanguage;
1449 if (systemLocale != null) {
1450 final String language = systemLocale.getLanguage();
1451 final boolean hasLanguage = !TextUtils.isEmpty(language);
1452 final String country = systemLocale.getCountry();
1453 final boolean hasCountry = !TextUtils.isEmpty(country);
1454 final String variant = systemLocale.getVariant();
1455 final boolean hasVariant = !TextUtils.isEmpty(variant);
1456 if (hasLanguage && hasCountry && hasVariant) {
1457 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
1458 } else {
1459 systemLocaleLanguageCountryVariant = null;
1460 }
1461 if (hasLanguage && hasCountry) {
1462 systemLocaleLanguageCountry = new Locale(language, country);
1463 } else {
1464 systemLocaleLanguageCountry = null;
1465 }
1466 if (hasLanguage) {
1467 systemLocaleLanguage = new Locale(language);
1468 } else {
1469 systemLocaleLanguage = null;
1470 }
1471 } else {
1472 systemLocaleLanguageCountryVariant = null;
1473 systemLocaleLanguageCountry = null;
1474 systemLocaleLanguage = null;
1475 }
1476
1477 final ArrayList<Locale> locales = new ArrayList<>();
1478 if (systemLocaleLanguageCountryVariant != null) {
1479 locales.add(systemLocaleLanguageCountryVariant);
1480 }
1481
1482 if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
1483 if (systemLocaleLanguageCountry != null) {
1484 // If the system language is English, and the region is also explicitly specified,
1485 // following fallback order will be applied.
1486 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
1487 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
1488 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
1489 // - en
1490 if (systemLocaleLanguageCountry != null) {
1491 locales.add(systemLocaleLanguageCountry);
1492 }
1493 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
1494 locales.add(LOCALE_EN_US);
1495 }
1496 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
1497 locales.add(LOCALE_EN_GB);
1498 }
1499 locales.add(Locale.ENGLISH);
1500 } else {
1501 // If the system language is English, but no region is specified, following
1502 // fallback order will be applied.
1503 // - en
1504 // - en_US
1505 // - en_GB
1506 locales.add(Locale.ENGLISH);
1507 locales.add(LOCALE_EN_US);
1508 locales.add(LOCALE_EN_GB);
1509 }
1510 } else {
1511 // If the system language is not English, the fallback order will be
1512 // - systemLocaleLanguageCountry [if non-null]
1513 // - systemLocaleLanguage [if non-null]
1514 // - en_US
1515 // - en_GB
1516 // - en
1517 if (systemLocaleLanguageCountry != null) {
1518 locales.add(systemLocaleLanguageCountry);
1519 }
1520 if (systemLocaleLanguage != null) {
1521 locales.add(systemLocaleLanguage);
1522 }
1523 locales.add(LOCALE_EN_US);
1524 locales.add(LOCALE_EN_GB);
1525 locales.add(Locale.ENGLISH);
1526 }
1527 return locales;
1528 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +09001529}