blob: 63d018fce01c3c6487cd4931cfdfe69505aa04e4 [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
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +090023import android.content.pm.PackageManager.NameNotFoundException;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090024import android.content.res.Resources;
25import android.provider.Settings;
26import android.provider.Settings.SettingNotFoundException;
27import android.text.TextUtils;
28import android.util.Pair;
29import android.util.Slog;
30import android.view.inputmethod.InputMethodInfo;
31import android.view.inputmethod.InputMethodSubtype;
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +090032import android.view.textservice.SpellCheckerInfo;
33import android.view.textservice.TextServicesManager;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090034
35import java.util.ArrayList;
36import java.util.HashMap;
37import java.util.List;
38import java.util.Locale;
39
40/**
41 * InputMethodManagerUtils contains some static methods that provides IME informations.
42 * This methods are supposed to be used in both the framework and the Settings application.
43 */
44public class InputMethodUtils {
45 public static final boolean DEBUG = false;
46 public static final int NOT_A_SUBTYPE_ID = -1;
47 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
48 public static final String SUBTYPE_MODE_VOICE = "voice";
49 private static final String TAG = "InputMethodUtils";
50 private static final Locale ENGLISH_LOCALE = new Locale("en");
51 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
52 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
53 "EnabledWhenDefaultIsNotAsciiCapable";
54 private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
55
56 private InputMethodUtils() {
57 // This utility class is not publicly instantiable.
58 }
59
Satoshi Kataoka0766eb02013-07-31 18:30:13 +090060 // ----------------------------------------------------------------------
61 // Utilities for debug
62 public static String getStackTrace() {
63 final StringBuilder sb = new StringBuilder();
64 try {
65 throw new RuntimeException();
66 } catch (RuntimeException e) {
67 final StackTraceElement[] frames = e.getStackTrace();
68 // Start at 1 because the first frame is here and we don't care about it
69 for (int j = 1; j < frames.length; ++j) {
70 sb.append(frames[j].toString() + "\n");
71 }
72 }
73 return sb.toString();
74 }
Satoshi Kataoka87c29142013-07-31 23:11:54 +090075
76 public static String getApiCallStack() {
77 String apiCallStack = "";
78 try {
79 throw new RuntimeException();
80 } catch (RuntimeException e) {
81 final StackTraceElement[] frames = e.getStackTrace();
82 for (int j = 1; j < frames.length; ++j) {
83 final String tempCallStack = frames[j].toString();
84 if (TextUtils.isEmpty(apiCallStack)) {
85 // Overwrite apiCallStack if it's empty
86 apiCallStack = tempCallStack;
87 } else if (tempCallStack.indexOf("Transact(") < 0) {
88 // Overwrite apiCallStack if it's not a binder call
89 apiCallStack = tempCallStack;
90 } else {
91 break;
92 }
93 }
94 }
95 return apiCallStack;
96 }
Satoshi Kataoka0766eb02013-07-31 18:30:13 +090097 // ----------------------------------------------------------------------
98
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +090099 public static boolean isSystemIme(InputMethodInfo inputMethod) {
100 return (inputMethod.getServiceInfo().applicationInfo.flags
101 & ApplicationInfo.FLAG_SYSTEM) != 0;
102 }
103
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900104 public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900105 if (!isSystemIme(imi)) {
106 return false;
107 }
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900108 return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD);
109 }
110
111 private static boolean isSystemAuxilialyImeThatHashAutomaticSubtype(InputMethodInfo imi) {
112 if (!isSystemIme(imi)) {
113 return false;
114 }
115 if (!imi.isAuxiliaryIme()) {
116 return false;
117 }
118 final int subtypeCount = imi.getSubtypeCount();
119 for (int i = 0; i < subtypeCount; ++i) {
120 final InputMethodSubtype s = imi.getSubtypeAt(i);
121 if (s.overridesImplicitlyEnabledSubtype()) {
122 return true;
123 }
124 }
125 return false;
126 }
127
128 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(
129 Context context, boolean isSystemReady, ArrayList<InputMethodInfo> imis) {
130 final ArrayList<InputMethodInfo> retval = new ArrayList<InputMethodInfo>();
131 boolean auxilialyImeAdded = false;
132 for (int i = 0; i < imis.size(); ++i) {
133 final InputMethodInfo imi = imis.get(i);
134 if (isDefaultEnabledIme(isSystemReady, imi, context)) {
135 retval.add(imi);
136 if (imi.isAuxiliaryIme()) {
137 auxilialyImeAdded = true;
138 }
139 }
140 }
141 if (auxilialyImeAdded) {
142 return retval;
143 }
144 for (int i = 0; i < imis.size(); ++i) {
145 final InputMethodInfo imi = imis.get(i);
146 if (isSystemAuxilialyImeThatHashAutomaticSubtype(imi)) {
147 retval.add(imi);
148 }
149 }
150 return retval;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900151 }
152
153 // TODO: Rename isSystemDefaultImeThatHasCurrentLanguageSubtype
154 public static boolean isValidSystemDefaultIme(
155 boolean isSystemReady, InputMethodInfo imi, Context context) {
156 if (!isSystemReady) {
157 return false;
158 }
159 if (!isSystemIme(imi)) {
160 return false;
161 }
162 if (imi.getIsDefaultResourceId() != 0) {
163 try {
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900164 if (imi.isDefault(context) && containsSubtypeOf(
165 imi, context.getResources().getConfiguration().locale.getLanguage(),
166 null /* mode */)) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900167 return true;
168 }
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900169 } catch (Resources.NotFoundException ex) {
170 }
171 }
172 if (imi.getSubtypeCount() == 0) {
173 Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName());
174 }
175 return false;
176 }
177
178 public static boolean isDefaultEnabledIme(
179 boolean isSystemReady, InputMethodInfo imi, Context context) {
180 return isValidSystemDefaultIme(isSystemReady, imi, context)
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900181 || isSystemImeThatHasEnglishKeyboardSubtype(imi);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900182 }
183
Satoshi Kataokaeb219ee2013-07-29 18:50:49 +0900184 public static boolean containsSubtypeOf(InputMethodInfo imi, String language, String mode) {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900185 final int N = imi.getSubtypeCount();
186 for (int i = 0; i < N; ++i) {
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900187 if (!imi.getSubtypeAt(i).getLocale().startsWith(language)) {
188 continue;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900189 }
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900190 if(!TextUtils.isEmpty(mode) && !imi.getSubtypeAt(i).getMode().equalsIgnoreCase(mode)) {
191 continue;
192 }
193 return true;
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900194 }
195 return false;
196 }
197
198 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
199 ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
200 final int subtypeCount = imi.getSubtypeCount();
201 for (int i = 0; i < subtypeCount; ++i) {
202 subtypes.add(imi.getSubtypeAt(i));
203 }
204 return subtypes;
205 }
206
207 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
208 InputMethodInfo imi, String mode) {
209 ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
210 final int subtypeCount = imi.getSubtypeCount();
211 for (int i = 0; i < subtypeCount; ++i) {
212 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
213 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
214 subtypes.add(subtype);
215 }
216 }
217 return subtypes;
218 }
219
220 public static InputMethodInfo getMostApplicableDefaultIME(
221 List<InputMethodInfo> enabledImes) {
222 if (enabledImes != null && enabledImes.size() > 0) {
223 // We'd prefer to fall back on a system IME, since that is safer.
224 int i = enabledImes.size();
225 int firstFoundSystemIme = -1;
226 while (i > 0) {
227 i--;
228 final InputMethodInfo imi = enabledImes.get(i);
Satoshi Kataokaf1367b72013-01-25 17:20:12 +0900229 if (InputMethodUtils.isSystemImeThatHasEnglishKeyboardSubtype(imi)
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900230 && !imi.isAuxiliaryIme()) {
231 return imi;
232 }
233 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)
234 && !imi.isAuxiliaryIme()) {
235 firstFoundSystemIme = i;
236 }
237 }
238 return enabledImes.get(Math.max(firstFoundSystemIme, 0));
239 }
240 return null;
241 }
242
243 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
244 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
245 }
246
247 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
248 if (imi != null) {
249 final int subtypeCount = imi.getSubtypeCount();
250 for (int i = 0; i < subtypeCount; ++i) {
251 InputMethodSubtype ims = imi.getSubtypeAt(i);
252 if (subtypeHashCode == ims.hashCode()) {
253 return i;
254 }
255 }
256 }
257 return NOT_A_SUBTYPE_ID;
258 }
259
260 private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
261 Resources res, InputMethodInfo imi) {
262 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
263 final String systemLocale = res.getConfiguration().locale.toString();
264 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>();
265 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap =
266 new HashMap<String, InputMethodSubtype>();
267 final int N = subtypes.size();
268 for (int i = 0; i < N; ++i) {
269 // scan overriding implicitly enabled subtypes.
270 InputMethodSubtype subtype = subtypes.get(i);
271 if (subtype.overridesImplicitlyEnabledSubtype()) {
272 final String mode = subtype.getMode();
273 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
274 applicableModeAndSubtypesMap.put(mode, subtype);
275 }
276 }
277 }
278 if (applicableModeAndSubtypesMap.size() > 0) {
279 return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values());
280 }
281 for (int i = 0; i < N; ++i) {
282 final InputMethodSubtype subtype = subtypes.get(i);
283 final String locale = subtype.getLocale();
284 final String mode = subtype.getMode();
285 // When system locale starts with subtype's locale, that subtype will be applicable
286 // for system locale
287 // For instance, it's clearly applicable for cases like system locale = en_US and
288 // subtype = en, but it is not necessarily considered applicable for cases like system
289 // locale = en and subtype = en_US.
290 // We just call systemLocale.startsWith(locale) in this function because there is no
291 // need to find applicable subtypes aggressively unlike
292 // findLastResortApplicableSubtypeLocked.
293 if (systemLocale.startsWith(locale)) {
294 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
295 // If more applicable subtypes are contained, skip.
296 if (applicableSubtype != null) {
297 if (systemLocale.equals(applicableSubtype.getLocale())) continue;
298 if (!systemLocale.equals(locale)) continue;
299 }
300 applicableModeAndSubtypesMap.put(mode, subtype);
301 }
302 }
303 final InputMethodSubtype keyboardSubtype
304 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
305 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>(
306 applicableModeAndSubtypesMap.values());
307 if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
308 for (int i = 0; i < N; ++i) {
309 final InputMethodSubtype subtype = subtypes.get(i);
310 final String mode = subtype.getMode();
311 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
312 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
313 applicableSubtypes.add(subtype);
314 }
315 }
316 }
317 if (keyboardSubtype == null) {
318 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
319 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
320 if (lastResortKeyboardSubtype != null) {
321 applicableSubtypes.add(lastResortKeyboardSubtype);
322 }
323 }
324 return applicableSubtypes;
325 }
326
327 private static List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
328 Context context, InputMethodInfo imi, List<InputMethodSubtype> enabledSubtypes,
329 boolean allowsImplicitlySelectedSubtypes) {
330 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
331 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
332 context.getResources(), imi);
333 }
334 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
335 }
336
337 /**
338 * If there are no selected subtypes, tries finding the most applicable one according to the
339 * given locale.
340 * @param subtypes this function will search the most applicable subtype in subtypes
341 * @param mode subtypes will be filtered by mode
342 * @param locale subtypes will be filtered by locale
343 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
344 * it will return the first subtype matched with mode
345 * @return the most applicable subtypeId
346 */
347 public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
348 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
349 boolean canIgnoreLocaleAsLastResort) {
350 if (subtypes == null || subtypes.size() == 0) {
351 return null;
352 }
353 if (TextUtils.isEmpty(locale)) {
354 locale = res.getConfiguration().locale.toString();
355 }
356 final String language = locale.substring(0, 2);
357 boolean partialMatchFound = false;
358 InputMethodSubtype applicableSubtype = null;
359 InputMethodSubtype firstMatchedModeSubtype = null;
360 final int N = subtypes.size();
361 for (int i = 0; i < N; ++i) {
362 InputMethodSubtype subtype = subtypes.get(i);
363 final String subtypeLocale = subtype.getLocale();
364 // An applicable subtype should match "mode". If mode is null, mode will be ignored,
365 // and all subtypes with all modes can be candidates.
366 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
367 if (firstMatchedModeSubtype == null) {
368 firstMatchedModeSubtype = subtype;
369 }
370 if (locale.equals(subtypeLocale)) {
371 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
372 applicableSubtype = subtype;
373 break;
374 } else if (!partialMatchFound && subtypeLocale.startsWith(language)) {
375 // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
376 applicableSubtype = subtype;
377 partialMatchFound = true;
378 }
379 }
380 }
381
382 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
383 return firstMatchedModeSubtype;
384 }
385
386 // The first subtype applicable to the system locale will be defined as the most applicable
387 // subtype.
388 if (DEBUG) {
389 if (applicableSubtype != null) {
390 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
391 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
392 }
393 }
394 return applicableSubtype;
395 }
396
397 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
398 if (subtype == null) return true;
399 return !subtype.isAuxiliary();
400 }
401
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900402 public static void setNonSelectedSystemImesDisabledUntilUsed(
403 PackageManager packageManager, List<InputMethodInfo> enabledImis) {
404 if (DEBUG) {
405 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
406 }
407 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
408 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
409 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
410 return;
411 }
412 // Only the current spell checker should be treated as an enabled one.
413 final SpellCheckerInfo currentSpellChecker =
414 TextServicesManager.getInstance().getCurrentSpellChecker();
415 for (final String packageName : systemImesDisabledUntilUsed) {
416 if (DEBUG) {
417 Slog.d(TAG, "check " + packageName);
418 }
419 boolean enabledIme = false;
420 for (int j = 0; j < enabledImis.size(); ++j) {
421 final InputMethodInfo imi = enabledImis.get(j);
422 if (packageName.equals(imi.getPackageName())) {
423 enabledIme = true;
424 break;
425 }
426 }
427 if (enabledIme) {
428 // enabled ime. skip
429 continue;
430 }
431 if (currentSpellChecker != null
432 && packageName.equals(currentSpellChecker.getPackageName())) {
433 // enabled spell checker. skip
434 if (DEBUG) {
435 Slog.d(TAG, packageName + " is the current spell checker. skip");
436 }
437 continue;
438 }
439 ApplicationInfo ai = null;
440 try {
441 ai = packageManager.getApplicationInfo(packageName,
442 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS);
443 } catch (NameNotFoundException e) {
444 Slog.w(TAG, "NameNotFoundException: " + packageName, e);
445 }
446 if (ai == null) {
447 // No app found for packageName
448 continue;
449 }
450 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
451 if (!isSystemPackage) {
452 continue;
453 }
454 setDisabledUntilUsed(packageManager, packageName);
455 }
456 }
457
458 private static void setDisabledUntilUsed(PackageManager packageManager, String packageName) {
459 final int state = packageManager.getApplicationEnabledSetting(packageName);
460 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
461 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
462 if (DEBUG) {
463 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
464 }
465 packageManager.setApplicationEnabledSetting(packageName,
466 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 0);
467 } else {
468 if (DEBUG) {
469 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
470 }
471 }
472 }
473
Satoshi Kataokab2827262013-07-04 19:43:14 +0900474 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
475 InputMethodSubtype subtype) {
476 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
477 return subtype != null
478 ? TextUtils.concat(subtype.getDisplayName(context,
479 imi.getPackageName(), imi.getServiceInfo().applicationInfo),
480 (TextUtils.isEmpty(imiLabel) ?
481 "" : " - " + imiLabel))
482 : imiLabel;
483 }
484
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900485 /**
486 * Utility class for putting and getting settings for InputMethod
487 * TODO: Move all putters and getters of settings to this class.
488 */
489 public static class InputMethodSettings {
490 // The string for enabled input method is saved as follows:
491 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
492 private static final char INPUT_METHOD_SEPARATER = ':';
493 private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';';
494 private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
495 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER);
496
497 private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
498 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER);
499
500 private final Resources mRes;
501 private final ContentResolver mResolver;
502 private final HashMap<String, InputMethodInfo> mMethodMap;
503 private final ArrayList<InputMethodInfo> mMethodList;
504
505 private String mEnabledInputMethodsStrCache;
506 private int mCurrentUserId;
507
508 private static void buildEnabledInputMethodsSettingString(
509 StringBuilder builder, Pair<String, ArrayList<String>> pair) {
510 String id = pair.first;
511 ArrayList<String> subtypes = pair.second;
512 builder.append(id);
513 // Inputmethod and subtypes are saved in the settings as follows:
514 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
515 for (String subtypeId: subtypes) {
516 builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId);
517 }
518 }
519
520 public InputMethodSettings(
521 Resources res, ContentResolver resolver,
522 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
523 int userId) {
524 setCurrentUserId(userId);
525 mRes = res;
526 mResolver = resolver;
527 mMethodMap = methodMap;
528 mMethodList = methodList;
529 }
530
531 public void setCurrentUserId(int userId) {
532 if (DEBUG) {
Satoshi Kataokaed1cdb22013-04-17 16:41:58 +0900533 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId);
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900534 }
535 // IMMS settings are kept per user, so keep track of current user
536 mCurrentUserId = userId;
537 }
538
539 public List<InputMethodInfo> getEnabledInputMethodListLocked() {
540 return createEnabledInputMethodListLocked(
541 getEnabledInputMethodsAndSubtypeListLocked());
542 }
543
544 public List<Pair<InputMethodInfo, ArrayList<String>>>
545 getEnabledInputMethodAndSubtypeHashCodeListLocked() {
546 return createEnabledInputMethodAndSubtypeHashCodeListLocked(
547 getEnabledInputMethodsAndSubtypeListLocked());
548 }
549
550 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
551 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
552 List<InputMethodSubtype> enabledSubtypes =
553 getEnabledInputMethodSubtypeListLocked(imi);
554 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
555 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
556 context.getResources(), imi);
557 }
558 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
559 }
560
Satoshi Kataoka7ce7f322013-08-05 17:12:28 +0900561 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900562 InputMethodInfo imi) {
563 List<Pair<String, ArrayList<String>>> imsList =
564 getEnabledInputMethodsAndSubtypeListLocked();
565 ArrayList<InputMethodSubtype> enabledSubtypes =
566 new ArrayList<InputMethodSubtype>();
567 if (imi != null) {
568 for (Pair<String, ArrayList<String>> imsPair : imsList) {
569 InputMethodInfo info = mMethodMap.get(imsPair.first);
570 if (info != null && info.getId().equals(imi.getId())) {
571 final int subtypeCount = info.getSubtypeCount();
572 for (int i = 0; i < subtypeCount; ++i) {
573 InputMethodSubtype ims = info.getSubtypeAt(i);
574 for (String s: imsPair.second) {
575 if (String.valueOf(ims.hashCode()).equals(s)) {
576 enabledSubtypes.add(ims);
577 }
578 }
579 }
580 break;
581 }
582 }
583 }
584 return enabledSubtypes;
585 }
586
587 // At the initial boot, the settings for input methods are not set,
588 // so we need to enable IME in that case.
589 public void enableAllIMEsIfThereIsNoEnabledIME() {
590 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
591 StringBuilder sb = new StringBuilder();
592 final int N = mMethodList.size();
593 for (int i = 0; i < N; i++) {
594 InputMethodInfo imi = mMethodList.get(i);
595 Slog.i(TAG, "Adding: " + imi.getId());
596 if (i > 0) sb.append(':');
597 sb.append(imi.getId());
598 }
599 putEnabledInputMethodsStr(sb.toString());
600 }
601 }
602
603 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
604 ArrayList<Pair<String, ArrayList<String>>> imsList
605 = new ArrayList<Pair<String, ArrayList<String>>>();
606 final String enabledInputMethodsStr = getEnabledInputMethodsStr();
607 if (TextUtils.isEmpty(enabledInputMethodsStr)) {
608 return imsList;
609 }
610 mInputMethodSplitter.setString(enabledInputMethodsStr);
611 while (mInputMethodSplitter.hasNext()) {
612 String nextImsStr = mInputMethodSplitter.next();
613 mSubtypeSplitter.setString(nextImsStr);
614 if (mSubtypeSplitter.hasNext()) {
615 ArrayList<String> subtypeHashes = new ArrayList<String>();
616 // The first element is ime id.
617 String imeId = mSubtypeSplitter.next();
618 while (mSubtypeSplitter.hasNext()) {
619 subtypeHashes.add(mSubtypeSplitter.next());
620 }
621 imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes));
622 }
623 }
624 return imsList;
625 }
626
627 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
628 if (reloadInputMethodStr) {
629 getEnabledInputMethodsStr();
630 }
631 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
632 // Add in the newly enabled input method.
633 putEnabledInputMethodsStr(id);
634 } else {
635 putEnabledInputMethodsStr(
636 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id);
637 }
638 }
639
640 /**
641 * Build and put a string of EnabledInputMethods with removing specified Id.
642 * @return the specified id was removed or not.
643 */
644 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
645 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
646 boolean isRemoved = false;
647 boolean needsAppendSeparator = false;
648 for (Pair<String, ArrayList<String>> ims: imsList) {
649 String curId = ims.first;
650 if (curId.equals(id)) {
651 // We are disabling this input method, and it is
652 // currently enabled. Skip it to remove from the
653 // new list.
654 isRemoved = true;
655 } else {
656 if (needsAppendSeparator) {
657 builder.append(INPUT_METHOD_SEPARATER);
658 } else {
659 needsAppendSeparator = true;
660 }
661 buildEnabledInputMethodsSettingString(builder, ims);
662 }
663 }
664 if (isRemoved) {
665 // Update the setting with the new list of input methods.
666 putEnabledInputMethodsStr(builder.toString());
667 }
668 return isRemoved;
669 }
670
671 private List<InputMethodInfo> createEnabledInputMethodListLocked(
672 List<Pair<String, ArrayList<String>>> imsList) {
673 final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>();
674 for (Pair<String, ArrayList<String>> ims: imsList) {
675 InputMethodInfo info = mMethodMap.get(ims.first);
676 if (info != null) {
677 res.add(info);
678 }
679 }
680 return res;
681 }
682
683 private List<Pair<InputMethodInfo, ArrayList<String>>>
684 createEnabledInputMethodAndSubtypeHashCodeListLocked(
685 List<Pair<String, ArrayList<String>>> imsList) {
686 final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res
687 = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>();
688 for (Pair<String, ArrayList<String>> ims : imsList) {
689 InputMethodInfo info = mMethodMap.get(ims.first);
690 if (info != null) {
691 res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second));
692 }
693 }
694 return res;
695 }
696
697 private void putEnabledInputMethodsStr(String str) {
698 Settings.Secure.putStringForUser(
699 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId);
700 mEnabledInputMethodsStrCache = str;
701 if (DEBUG) {
702 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
703 }
704 }
705
Dianne Hackbornfd7aded2013-01-22 17:10:23 -0800706 public String getEnabledInputMethodsStr() {
Satoshi Kataoka8e303cc2013-01-11 15:55:28 +0900707 mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser(
708 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId);
709 if (DEBUG) {
710 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
711 + ", " + mCurrentUserId);
712 }
713 return mEnabledInputMethodsStrCache;
714 }
715
716 private void saveSubtypeHistory(
717 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
718 StringBuilder builder = new StringBuilder();
719 boolean isImeAdded = false;
720 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
721 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
722 newSubtypeId);
723 isImeAdded = true;
724 }
725 for (Pair<String, String> ime: savedImes) {
726 String imeId = ime.first;
727 String subtypeId = ime.second;
728 if (TextUtils.isEmpty(subtypeId)) {
729 subtypeId = NOT_A_SUBTYPE_ID_STR;
730 }
731 if (isImeAdded) {
732 builder.append(INPUT_METHOD_SEPARATER);
733 } else {
734 isImeAdded = true;
735 }
736 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
737 subtypeId);
738 }
739 // Remove the last INPUT_METHOD_SEPARATER
740 putSubtypeHistoryStr(builder.toString());
741 }
742
743 private void addSubtypeToHistory(String imeId, String subtypeId) {
744 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
745 for (Pair<String, String> ime: subtypeHistory) {
746 if (ime.first.equals(imeId)) {
747 if (DEBUG) {
748 Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
749 + ime.second);
750 }
751 // We should break here
752 subtypeHistory.remove(ime);
753 break;
754 }
755 }
756 if (DEBUG) {
757 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
758 }
759 saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
760 }
761
762 private void putSubtypeHistoryStr(String str) {
763 if (DEBUG) {
764 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
765 }
766 Settings.Secure.putStringForUser(
767 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId);
768 }
769
770 public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
771 // Gets the first one from the history
772 return getLastSubtypeForInputMethodLockedInternal(null);
773 }
774
775 public String getLastSubtypeForInputMethodLocked(String imeId) {
776 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
777 if (ime != null) {
778 return ime.second;
779 } else {
780 return null;
781 }
782 }
783
784 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
785 List<Pair<String, ArrayList<String>>> enabledImes =
786 getEnabledInputMethodsAndSubtypeListLocked();
787 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
788 for (Pair<String, String> imeAndSubtype : subtypeHistory) {
789 final String imeInTheHistory = imeAndSubtype.first;
790 // If imeId is empty, returns the first IME and subtype in the history
791 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
792 final String subtypeInTheHistory = imeAndSubtype.second;
793 final String subtypeHashCode =
794 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
795 enabledImes, imeInTheHistory, subtypeInTheHistory);
796 if (!TextUtils.isEmpty(subtypeHashCode)) {
797 if (DEBUG) {
798 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
799 }
800 return new Pair<String, String>(imeInTheHistory, subtypeHashCode);
801 }
802 }
803 }
804 if (DEBUG) {
805 Slog.d(TAG, "No enabled IME found in the history");
806 }
807 return null;
808 }
809
810 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
811 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
812 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
813 if (enabledIme.first.equals(imeId)) {
814 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
815 final InputMethodInfo imi = mMethodMap.get(imeId);
816 if (explicitlyEnabledSubtypes.size() == 0) {
817 // If there are no explicitly enabled subtypes, applicable subtypes are
818 // enabled implicitly.
819 // If IME is enabled and no subtypes are enabled, applicable subtypes
820 // are enabled implicitly, so needs to treat them to be enabled.
821 if (imi != null && imi.getSubtypeCount() > 0) {
822 List<InputMethodSubtype> implicitlySelectedSubtypes =
823 getImplicitlyApplicableSubtypesLocked(mRes, imi);
824 if (implicitlySelectedSubtypes != null) {
825 final int N = implicitlySelectedSubtypes.size();
826 for (int i = 0; i < N; ++i) {
827 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
828 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
829 return subtypeHashCode;
830 }
831 }
832 }
833 }
834 } else {
835 for (String s: explicitlyEnabledSubtypes) {
836 if (s.equals(subtypeHashCode)) {
837 // If both imeId and subtypeId are enabled, return subtypeId.
838 try {
839 final int hashCode = Integer.valueOf(subtypeHashCode);
840 // Check whether the subtype id is valid or not
841 if (isValidSubtypeId(imi, hashCode)) {
842 return s;
843 } else {
844 return NOT_A_SUBTYPE_ID_STR;
845 }
846 } catch (NumberFormatException e) {
847 return NOT_A_SUBTYPE_ID_STR;
848 }
849 }
850 }
851 }
852 // If imeId was enabled but subtypeId was disabled.
853 return NOT_A_SUBTYPE_ID_STR;
854 }
855 }
856 // If both imeId and subtypeId are disabled, return null
857 return null;
858 }
859
860 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
861 ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>();
862 final String subtypeHistoryStr = getSubtypeHistoryStr();
863 if (TextUtils.isEmpty(subtypeHistoryStr)) {
864 return imsList;
865 }
866 mInputMethodSplitter.setString(subtypeHistoryStr);
867 while (mInputMethodSplitter.hasNext()) {
868 String nextImsStr = mInputMethodSplitter.next();
869 mSubtypeSplitter.setString(nextImsStr);
870 if (mSubtypeSplitter.hasNext()) {
871 String subtypeId = NOT_A_SUBTYPE_ID_STR;
872 // The first element is ime id.
873 String imeId = mSubtypeSplitter.next();
874 while (mSubtypeSplitter.hasNext()) {
875 subtypeId = mSubtypeSplitter.next();
876 break;
877 }
878 imsList.add(new Pair<String, String>(imeId, subtypeId));
879 }
880 }
881 return imsList;
882 }
883
884 private String getSubtypeHistoryStr() {
885 if (DEBUG) {
886 Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser(
887 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId));
888 }
889 return Settings.Secure.getStringForUser(
890 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId);
891 }
892
893 public void putSelectedInputMethod(String imeId) {
894 if (DEBUG) {
895 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
896 + mCurrentUserId);
897 }
898 Settings.Secure.putStringForUser(
899 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId);
900 }
901
902 public void putSelectedSubtype(int subtypeId) {
903 if (DEBUG) {
904 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
905 + mCurrentUserId);
906 }
907 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
908 subtypeId, mCurrentUserId);
909 }
910
911 public String getDisabledSystemInputMethods() {
912 return Settings.Secure.getStringForUser(
913 mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId);
914 }
915
916 public String getSelectedInputMethod() {
917 if (DEBUG) {
918 Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser(
919 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId)
920 + ", " + mCurrentUserId);
921 }
922 return Settings.Secure.getStringForUser(
923 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId);
924 }
925
926 public boolean isSubtypeSelected() {
927 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
928 }
929
930 private int getSelectedInputMethodSubtypeHashCode() {
931 try {
932 return Settings.Secure.getIntForUser(
933 mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId);
934 } catch (SettingNotFoundException e) {
935 return NOT_A_SUBTYPE_ID;
936 }
937 }
938
939 public int getCurrentUserId() {
940 return mCurrentUserId;
941 }
942
943 public int getSelectedInputMethodSubtypeId(String selectedImiId) {
944 final InputMethodInfo imi = mMethodMap.get(selectedImiId);
945 if (imi == null) {
946 return NOT_A_SUBTYPE_ID;
947 }
948 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
949 return getSubtypeIdFromHashCode(imi, subtypeHashCode);
950 }
951
952 public void saveCurrentInputMethodAndSubtypeToHistory(
953 String curMethodId, InputMethodSubtype currentSubtype) {
954 String subtypeId = NOT_A_SUBTYPE_ID_STR;
955 if (currentSubtype != null) {
956 subtypeId = String.valueOf(currentSubtype.hashCode());
957 }
958 if (canAddToLastInputMethod(currentSubtype)) {
959 addSubtypeToHistory(curMethodId, subtypeId);
960 }
961 }
962 }
963}