blob: 95df99855dcfaf9faad007ef1cd926ed04cac073 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.inputmethod;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* This class provides utility methods to handle and manage {@link InputMethodSubtype} for
* {@link InputMethodManagerService}.
*
* <p>This class is intentionally package-private. Utility methods here are tightly coupled with
* implementation details in {@link InputMethodManagerService}. Hence this class is not suitable
* for other components to directly use.</p>
*/
final class SubtypeUtils {
private static final String TAG = "SubtypeUtils";
public static final boolean DEBUG = false;
static final String SUBTYPE_MODE_ANY = null;
static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
static final int NOT_A_SUBTYPE_ID = -1;
private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
"EnabledWhenDefaultIsNotAsciiCapable";
// A temporary workaround for the performance concerns in
// #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
// TODO: Optimize all the critical paths including this one.
// TODO(b/235661780): Make the cache supports multi-users.
private static final Object sCacheLock = new Object();
@GuardedBy("sCacheLock")
private static LocaleList sCachedSystemLocales;
@GuardedBy("sCacheLock")
private static InputMethodInfo sCachedInputMethodInfo;
@GuardedBy("sCacheLock")
private static ArrayList<InputMethodSubtype> sCachedResult;
static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale,
boolean checkCountry, String mode) {
if (locale == null) {
return false;
}
final int numSubtypes = imi.getSubtypeCount();
for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype subtype = imi.getSubtypeAt(i);
if (checkCountry) {
final Locale subtypeLocale = subtype.getLocaleObject();
if (subtypeLocale == null
|| !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())
|| !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
continue;
}
} else {
final Locale subtypeLocale = new Locale(LocaleUtils.getLanguageFromLocaleString(
subtype.getLocale()));
if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
continue;
}
}
if (TextUtils.isEmpty(mode) || mode.equalsIgnoreCase(subtype.getMode())) {
return true;
}
}
return false;
}
static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
final int subtypeCount = imi.getSubtypeCount();
for (int i = 0; i < subtypeCount; ++i) {
subtypes.add(imi.getSubtypeAt(i));
}
return subtypes;
}
static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
}
static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
if (imi != null) {
final int subtypeCount = imi.getSubtypeCount();
for (int i = 0; i < subtypeCount; ++i) {
InputMethodSubtype ims = imi.getSubtypeAt(i);
if (subtypeHashCode == ims.hashCode()) {
return i;
}
}
}
return NOT_A_SUBTYPE_ID;
}
private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
source -> source != null ? source.getLocaleObject() : null;
@VisibleForTesting
@NonNull
static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
@NonNull LocaleList systemLocales, InputMethodInfo imi) {
synchronized (sCacheLock) {
// We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
// it does not check if subtypes are also identical.
if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
return new ArrayList<>(sCachedResult);
}
}
// Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
// TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
// LocaleList rather than Resource.
final ArrayList<InputMethodSubtype> result =
getImplicitlyApplicableSubtypesLockedImpl(systemLocales, imi);
synchronized (sCacheLock) {
// Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
sCachedSystemLocales = systemLocales;
sCachedInputMethodInfo = imi;
sCachedResult = new ArrayList<>(result);
}
return result;
}
private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
@NonNull LocaleList systemLocales, InputMethodInfo imi) {
final List<InputMethodSubtype> subtypes = getSubtypes(imi);
final String systemLocale = systemLocales.get(0).toString();
if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
final int numSubtypes = subtypes.size();
// Handle overridesImplicitlyEnabledSubtype mechanism.
final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>();
for (int i = 0; i < numSubtypes; ++i) {
// scan overriding implicitly enabled subtypes.
final InputMethodSubtype subtype = subtypes.get(i);
if (subtype.overridesImplicitlyEnabledSubtype()) {
final String mode = subtype.getMode();
if (!applicableModeAndSubtypesMap.containsKey(mode)) {
applicableModeAndSubtypesMap.put(mode, subtype);
}
}
}
if (applicableModeAndSubtypesMap.size() > 0) {
return new ArrayList<>(applicableModeAndSubtypesMap.values());
}
final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
new ArrayMap<>();
final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype subtype = subtypes.get(i);
final String mode = subtype.getMode();
if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
keyboardSubtypes.add(subtype);
} else {
if (!nonKeyboardSubtypesMap.containsKey(mode)) {
nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
}
nonKeyboardSubtypesMap.get(mode).add(subtype);
}
}
final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
applicableSubtypes);
if (!applicableSubtypes.isEmpty()) {
boolean hasAsciiCapableKeyboard = false;
final int numApplicationSubtypes = applicableSubtypes.size();
for (int i = 0; i < numApplicationSubtypes; ++i) {
final InputMethodSubtype subtype = applicableSubtypes.get(i);
if (subtype.isAsciiCapable()) {
hasAsciiCapableKeyboard = true;
break;
}
}
if (!hasAsciiCapableKeyboard) {
final int numKeyboardSubtypes = keyboardSubtypes.size();
for (int i = 0; i < numKeyboardSubtypes; ++i) {
final InputMethodSubtype subtype = keyboardSubtypes.get(i);
final String mode = subtype.getMode();
if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
applicableSubtypes.add(subtype);
}
}
}
}
if (applicableSubtypes.isEmpty()) {
InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
if (lastResortKeyboardSubtype != null) {
applicableSubtypes.add(lastResortKeyboardSubtype);
}
}
// For each non-keyboard mode, extract subtypes with system locales.
for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
applicableSubtypes);
}
return applicableSubtypes;
}
/**
* If there are no selected subtypes, tries finding the most applicable one according to the
* given locale.
*
* @param subtypes a list of {@link InputMethodSubtype} to search
* @param mode the mode used for filtering subtypes
* @param locale the locale used for filtering subtypes
* @param canIgnoreLocaleAsLastResort when set to {@code true}, if this function can't find the
* most applicable subtype, it will return the first subtype
* matched with mode
*
* @return the most applicable subtypeId
*/
static InputMethodSubtype findLastResortApplicableSubtypeLocked(
List<InputMethodSubtype> subtypes, String mode, @NonNull String locale,
boolean canIgnoreLocaleAsLastResort) {
if (subtypes == null || subtypes.isEmpty()) {
return null;
}
final String language = LocaleUtils.getLanguageFromLocaleString(locale);
boolean partialMatchFound = false;
InputMethodSubtype applicableSubtype = null;
InputMethodSubtype firstMatchedModeSubtype = null;
final int numSubtypes = subtypes.size();
for (int i = 0; i < numSubtypes; ++i) {
InputMethodSubtype subtype = subtypes.get(i);
final String subtypeLocale = subtype.getLocale();
final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
// An applicable subtype should match "mode". If mode is null, mode will be ignored,
// and all subtypes with all modes can be candidates.
if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
if (firstMatchedModeSubtype == null) {
firstMatchedModeSubtype = subtype;
}
if (locale.equals(subtypeLocale)) {
// Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
applicableSubtype = subtype;
break;
} else if (!partialMatchFound && language.equals(subtypeLanguage)) {
// Partial match (e.g. system locale is "en_US" and subtype locale is "en")
applicableSubtype = subtype;
partialMatchFound = true;
}
}
}
if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
return firstMatchedModeSubtype;
}
// The first subtype applicable to the system locale will be defined as the most applicable
// subtype.
if (DEBUG) {
if (applicableSubtype != null) {
Slog.d(TAG, "Applicable InputMethodSubtype was found: "
+ applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
}
}
return applicableSubtype;
}
}