Fix loading of large IMEs
Test: manual atest
Fix: 261723412
Change-Id: I51703563414192ee778f30ab57390da1c1a5ded5
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 3baf5a2..cfb9dc8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3527,6 +3527,8 @@
public final class InputMethodInfo implements android.os.Parcelable {
ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, boolean, @NonNull String);
ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, int);
+ field public static final int COMPONENT_NAME_MAX_LENGTH = 1000; // 0x3e8
+ field public static final int MAX_IMES_PER_PACKAGE = 20; // 0x14
}
public final class InputMethodManager {
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index ec1badb..8b55494 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
@@ -83,6 +84,22 @@
public static final String ACTION_STYLUS_HANDWRITING_SETTINGS =
"android.view.inputmethod.action.STYLUS_HANDWRITING_SETTINGS";
+ /**
+ * Maximal length of a component name
+ * @hide
+ */
+ @TestApi
+ public static final int COMPONENT_NAME_MAX_LENGTH = 1000;
+
+ /**
+ * The maximum amount of IMEs that are loaded per package (in order).
+ * If a package contains more IMEs, they will be ignored and cannot be enabled.
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_IMES_PER_PACKAGE = 20;
+
static final String TAG = "InputMethodInfo";
/**
@@ -252,6 +269,13 @@
com.android.internal.R.styleable.InputMethod);
settingsActivityComponent = sa.getString(
com.android.internal.R.styleable.InputMethod_settingsActivity);
+ if ((si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH) || (
+ settingsActivityComponent != null
+ && settingsActivityComponent.length() > COMPONENT_NAME_MAX_LENGTH)) {
+ throw new XmlPullParserException(
+ "Activity name exceeds maximum of 1000 characters");
+ }
+
isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false);
isDefaultResId = sa.getResourceId(
com.android.internal.R.styleable.InputMethod_isDefault, 0);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f5875ab..6e1fe4d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2060,7 +2060,7 @@
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
- methodList, directBootAwareness);
+ methodList, directBootAwareness, mSettings.getEnabledInputMethodNames());
settings = new InputMethodSettings(mContext, methodMap, userId, true /* copyOnWrite */);
}
// filter caller's access to input methods
@@ -4050,7 +4050,7 @@
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
- methodList, DirectBootAwareness.AUTO);
+ methodList, DirectBootAwareness.AUTO, mSettings.getEnabledInputMethodNames());
final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
userId, false);
settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
@@ -4940,7 +4940,7 @@
static void queryInputMethodServicesInternal(Context context,
@UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
- @DirectBootAwareness int directBootAwareness) {
+ @DirectBootAwareness int directBootAwareness, List<String> enabledInputMethodList) {
final Context userAwareContext = context.getUserId() == userId
? context
: context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
@@ -4973,6 +4973,17 @@
methodList.ensureCapacity(services.size());
methodMap.ensureCapacity(services.size());
+ filterInputMethodServices(additionalSubtypeMap, methodMap, methodList,
+ enabledInputMethodList, userAwareContext, services);
+ }
+
+ static void filterInputMethodServices(
+ ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+ ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
+ List<String> enabledInputMethodList, Context userAwareContext,
+ List<ResolveInfo> services) {
+ final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>();
+
for (int i = 0; i < services.size(); ++i) {
ResolveInfo ri = services.get(i);
ServiceInfo si = ri.serviceInfo;
@@ -4992,10 +5003,21 @@
if (imi.isVrOnly()) {
continue; // Skip VR-only IME, which isn't supported for now.
}
- methodList.add(imi);
- methodMap.put(imi.getId(), imi);
- if (DEBUG) {
- Slog.d(TAG, "Found an input method " + imi);
+ final String packageName = si.packageName;
+ // only include IMEs which are from the system, enabled, or below the threshold
+ if (si.applicationInfo.isSystemApp() || enabledInputMethodList.contains(imi.getId())
+ || imiPackageCount.getOrDefault(packageName, 0)
+ < InputMethodInfo.MAX_IMES_PER_PACKAGE) {
+ imiPackageCount.put(packageName,
+ 1 + imiPackageCount.getOrDefault(packageName, 0));
+
+ methodList.add(imi);
+ methodMap.put(imi.getId(), imi);
+ if (DEBUG) {
+ Slog.d(TAG, "Found an input method " + imi);
+ }
+ } else if (DEBUG) {
+ Slog.d(TAG, "Found an input method, but ignored due threshold: " + imi);
}
} catch (Exception e) {
Slog.wtf(TAG, "Unable to load input method " + imeId, e);
@@ -5017,7 +5039,8 @@
mMyPackageMonitor.clearKnownImePackageNamesLocked();
queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(),
- mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO);
+ mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO,
+ mSettings.getEnabledInputMethodNames());
// Construct the set of possible IME packages for onPackageChanged() to avoid false
// negatives when the package state remains to be the same but only the component state is
@@ -5076,7 +5099,7 @@
reenableMinimumNonAuxSystemImes);
final int numImes = defaultEnabledIme.size();
for (int i = 0; i < numImes; ++i) {
- final InputMethodInfo imi = defaultEnabledIme.get(i);
+ final InputMethodInfo imi = defaultEnabledIme.get(i);
if (DEBUG) {
Slog.d(TAG, "--- enable ime = " + imi);
}
@@ -5376,7 +5399,8 @@
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
- methodMap, methodList, DirectBootAwareness.AUTO);
+ methodMap, methodList, DirectBootAwareness.AUTO,
+ mSettings.getEnabledInputMethodNames());
return methodMap;
}
@@ -6334,7 +6358,8 @@
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
- methodMap, methodList, DirectBootAwareness.AUTO);
+ methodMap, methodList, DirectBootAwareness.AUTO,
+ mSettings.getEnabledInputMethodNames());
final InputMethodSettings settings = new InputMethodSettings(mContext,
methodMap, userId, false);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index 559eb53..17536fc 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -438,6 +438,15 @@
mSubtypeSplitter);
}
+ List<String> getEnabledInputMethodNames() {
+ List<String> result = new ArrayList<>();
+ for (Pair<String, ArrayList<String>> pair :
+ getEnabledInputMethodsAndSubtypeListLocked()) {
+ result.add(pair.first);
+ }
+ return result;
+ }
+
void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
if (reloadInputMethodStr) {
getEnabledInputMethodsStr();
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index 05a8b11..07ddda3 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -52,6 +52,10 @@
"android.test.runner",
],
+ data: [
+ ":SimpleTestIme",
+ ],
+
certificate: "platform",
platform_apis: true,
test_suites: ["device-tests"],
diff --git a/services/tests/InputMethodSystemServerTests/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/AndroidTest.xml
index 92be780..1371934 100644
--- a/services/tests/InputMethodSystemServerTests/AndroidTest.xml
+++ b/services/tests/InputMethodSystemServerTests/AndroidTest.xml
@@ -21,6 +21,7 @@
<option name="cleanup-apks" value="true" />
<option name="install-arg" value="-t" />
<option name="test-file-name" value="FrameworksInputMethodSystemServerTests.apk" />
+ <option name="test-file-name" value="SimpleTestIme.apk" />
</target_preparer>
<option name="test-tag" value="FrameworksInputMethodSystemServerTests" />
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
new file mode 100644
index 0000000..7cbfc52
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2023 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 static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.util.ArrayMap;
+import android.view.inputmethod.InputMethod;
+import android.view.inputmethod.InputMethodInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class InputMethodManagerServiceRestrictImeAmountTest extends
+ InputMethodManagerServiceTestBase {
+
+ @Test
+ public void testFilterInputMethodServices_loadsAllImesBelowThreshold() {
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ resolveInfoList.add(
+ createFakeResolveInfo("com.android.apps.inputmethod.simpleime", "IME" + i));
+ }
+
+ final List<InputMethodInfo> methodList = filterInputMethodServices(resolveInfoList,
+ List.of());
+ assertEquals(5, methodList.size());
+ }
+
+ @Test
+ public void testFilterInputMethodServices_ignoresImesBeyondThreshold() {
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+ for (int i = 0; i < 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE; i++) {
+ resolveInfoList.add(
+ createFakeResolveInfo("com.android.apps.inputmethod.simpleime", "IME" + i));
+ }
+
+ final List<InputMethodInfo> methodList = filterInputMethodServices(resolveInfoList,
+ List.of());
+ assertWithMessage("Filtered IMEs").that(methodList.size()).isEqualTo(
+ InputMethodInfo.MAX_IMES_PER_PACKAGE);
+ }
+
+ @Test
+ public void testFilterInputMethodServices_loadsSystemImesBeyondThreshold() {
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+ for (int i = 0; i < 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE; i++) {
+ resolveInfoList.add(
+ createFakeSystemResolveInfo("com.android.apps.inputmethod.systemime",
+ "SystemIME" + i));
+ }
+
+ final List<InputMethodInfo> methodList = filterInputMethodServices(resolveInfoList,
+ List.of());
+ assertWithMessage("Filtered IMEs").that(methodList.size()).isEqualTo(
+ 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE);
+ }
+
+ @Test
+ public void testFilterInputMethodServices_ignoresImesBeyondThresholdFromTwoPackages() {
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+ for (int i = 0; i < 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE; i++) {
+ resolveInfoList.add(
+ createFakeResolveInfo("com.android.apps.inputmethod.simpleime1", "IME1_" + i));
+ }
+ for (int i = 0; i < 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE; i++) {
+ resolveInfoList.add(
+ createFakeResolveInfo("com.android.apps.inputmethod.simpleime2", "IME2_" + i));
+ }
+
+ final List<InputMethodInfo> methodList = filterInputMethodServices(resolveInfoList,
+ List.of());
+ assertWithMessage("Filtered IMEs").that(methodList.size()).isEqualTo(
+ 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE);
+ }
+
+ @Test
+ public void testFilterInputMethodServices_stillLoadsEnabledImesBeyondThreshold() {
+ final ResolveInfo enabledIme = createFakeResolveInfo(
+ "com.android.apps.inputmethod.simpleime_enabled", "EnabledIME");
+
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+ for (int i = 0; i < 2 * InputMethodInfo.MAX_IMES_PER_PACKAGE; i++) {
+ resolveInfoList.add(
+ createFakeResolveInfo("com.android.apps.inputmethod.simpleime", "IME" + i));
+ }
+ resolveInfoList.add(enabledIme);
+
+ final List<InputMethodInfo> methodList = filterInputMethodServices(resolveInfoList,
+ List.of(new ComponentName(enabledIme.serviceInfo.packageName,
+ enabledIme.serviceInfo.name).flattenToShortString()));
+
+ assertWithMessage("Filtered IMEs").that(methodList.size()).isEqualTo(
+ 1 + InputMethodInfo.MAX_IMES_PER_PACKAGE);
+ }
+
+ private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList,
+ List<String> enabledComponents) {
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+ InputMethodManagerService.filterInputMethodServices(new ArrayMap<>(), methodMap, methodList,
+ enabledComponents, mContext, resolveInfoList);
+ return methodList;
+ }
+
+ private ResolveInfo createFakeSystemResolveInfo(String packageName, String componentName) {
+ final ResolveInfo ime = createFakeResolveInfo(packageName, componentName);
+ ime.serviceInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM;
+ return ime;
+ }
+
+ private ResolveInfo createFakeResolveInfo(String packageName, String componentName) {
+ final ResolveInfo ime = getResolveInfo("com.android.apps.inputmethod.simpleime");
+ if (packageName != null) {
+ ime.serviceInfo.packageName = packageName;
+ }
+ if (componentName != null) {
+ ime.serviceInfo.name = componentName;
+ }
+ return ime;
+ }
+
+ private ResolveInfo getResolveInfo(String packageName) {
+ final int flags = PackageManager.GET_META_DATA
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+ final List<ResolveInfo> ime = mContext.getPackageManager().queryIntentServices(
+ new Intent(InputMethod.SERVICE_INTERFACE).setPackage(packageName),
+ PackageManager.ResolveInfoFlags.of(flags));
+ assertWithMessage("Loaded IMEs").that(ime.size()).isGreaterThan(0);
+ return ime.get(0);
+ }
+}