Fix loading of large IMEs

Test: manual atest
Fix: 261723412
Change-Id: I51703563414192ee778f30ab57390da1c1a5ded5
This commit is contained in:
Felix Stern
2023-02-16 17:48:27 +00:00
parent 37ea5f6833
commit eed1008252
7 changed files with 235 additions and 11 deletions

View File

@@ -3527,6 +3527,8 @@ package android.view.inputmethod {
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 {

View File

@@ -18,6 +18,7 @@ package android.view.inputmethod;
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 final class InputMethodInfo implements Parcelable {
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 @@ public final class InputMethodInfo implements Parcelable {
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);

View File

@@ -2060,7 +2060,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
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);

View File

@@ -438,6 +438,15 @@ final class InputMethodUtils {
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();

View File

@@ -52,6 +52,10 @@ android_test {
"android.test.runner",
],
data: [
":SimpleTestIme",
],
certificate: "platform",
platform_apis: true,
test_suites: ["device-tests"],

View File

@@ -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" />

View File

@@ -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);
}
}