Merge "Fix loading of large IMEs" into udc-dev
This commit is contained in:
@@ -3547,6 +3547,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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2079,7 +2079,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
|
||||
@@ -4136,7 +4136,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,
|
||||
@@ -5033,7 +5033,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 */);
|
||||
@@ -5066,6 +5066,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;
|
||||
@@ -5085,10 +5096,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);
|
||||
@@ -5110,7 +5132,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
|
||||
@@ -5169,7 +5192,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);
|
||||
}
|
||||
@@ -5469,7 +5492,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;
|
||||
}
|
||||
|
||||
@@ -6429,7 +6453,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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -52,6 +52,10 @@ android_test {
|
||||
"android.test.runner",
|
||||
],
|
||||
|
||||
data: [
|
||||
":SimpleTestIme",
|
||||
],
|
||||
|
||||
certificate: "platform",
|
||||
platform_apis: true,
|
||||
test_suites: ["device-tests"],
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user