Loading services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java 0 → 100644 +155 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.ArrayMap; import android.view.inputmethod.InputMethodSubtype; import java.util.Collection; import java.util.List; /** * An on-memory immutable data representation of subtype.xml, which contains so-called additional * {@link InputMethodSubtype}. * * <p>While the data structure could be also used for general purpose map from IME ID to * a list of {@link InputMethodSubtype}, unlike {@link InputMethodMap} this particular data * structure is currently used only around additional {@link InputMethodSubtype}, which is why this * class is (still) called {@code AdditionalSubtypeMap} rather than {@code InputMethodSubtypeMap}. * </p> */ final class AdditionalSubtypeMap { /** * An empty {@link AdditionalSubtypeMap}. */ static final AdditionalSubtypeMap EMPTY_MAP = new AdditionalSubtypeMap(new ArrayMap<>()); @NonNull private final ArrayMap<String, List<InputMethodSubtype>> mMap; @AnyThread @NonNull private static AdditionalSubtypeMap createOrEmpty( @NonNull ArrayMap<String, List<InputMethodSubtype>> map) { return map.isEmpty() ? EMPTY_MAP : new AdditionalSubtypeMap(map); } /** * Create a new instance from the given {@link ArrayMap}. * * <p>This method effectively creates a new copy of map.</p> * * @param map An {@link ArrayMap} from which {@link AdditionalSubtypeMap} is to be created. * @return A {@link AdditionalSubtypeMap} that contains a new copy of {@code map}. */ @AnyThread @NonNull static AdditionalSubtypeMap of(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) { return createOrEmpty(map); } /** * Create a new instance of {@link AdditionalSubtypeMap} from an existing * {@link AdditionalSubtypeMap} by removing {@code key}, or return {@code map} itself if it does * not contain an entry of {@code key}. * * @param key The key to be removed from {@code map}. * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain * {@code key}, or {@code map} itself if it does not contain an entry of {@code key}. */ @AnyThread @NonNull AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull String key) { if (isEmpty() || !containsKey(key)) { return this; } final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); newMap.remove(key); return createOrEmpty(newMap); } /** * Create a new instance of {@link AdditionalSubtypeMap} from an existing * {@link AdditionalSubtypeMap} by removing {@code keys} or return {@code map} itself if it does * not contain any entry for {@code keys}. * * @param keys Keys to be removed from {@code map}. * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain * {@code keys}, or {@code map} itself if it does not contain any entry of {@code keys}. */ @AnyThread @NonNull AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull Collection<String> keys) { if (isEmpty()) { return this; } final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); return newMap.removeAll(keys) ? createOrEmpty(newMap) : this; } /** * Create a new instance of {@link AdditionalSubtypeMap} from an existing * {@link AdditionalSubtypeMap} by putting {@code key} and {@code value}. * * @param key Key to be put into {@code map}. * @param value Value to be put into {@code map}. * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to contain the * pair of {@code key} and {@code value}. */ @AnyThread @NonNull AdditionalSubtypeMap cloneWithPut( @Nullable String key, @NonNull List<InputMethodSubtype> value) { final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); newMap.put(key, value); return new AdditionalSubtypeMap(newMap); } private AdditionalSubtypeMap(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) { mMap = map; } @AnyThread @Nullable List<InputMethodSubtype> get(@Nullable String key) { return mMap.get(key); } @AnyThread boolean containsKey(@Nullable String key) { return mMap.containsKey(key); } @AnyThread boolean isEmpty() { return mMap.isEmpty(); } @AnyThread @NonNull Collection<String> keySet() { return mMap.keySet(); } @AnyThread int size() { return mMap.size(); } } services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +12 −14 Original line number Diff line number Diff line Loading @@ -108,12 +108,12 @@ final class AdditionalSubtypeUtils { * multiple threads are not calling this method at the same time for the same {@code userId}. * </p> * * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty * map deletes the file. * @param allSubtypes {@link AdditionalSubtypeMap} from IME ID to additional subtype list. * Passing an empty map deletes the file. * @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}. * @param userId The user ID to be associated with. */ static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, static void save(AdditionalSubtypeMap allSubtypes, InputMethodMap methodMap, @UserIdInt int userId) { final File inputMethodDir = getInputMethodDir(userId); Loading Loading @@ -142,7 +142,7 @@ final class AdditionalSubtypeUtils { } @VisibleForTesting static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, static void saveToFile(AdditionalSubtypeMap allSubtypes, InputMethodMap methodMap, AtomicFile subtypesFile) { // Safety net for the case that this function is called before methodMap is set. final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; Loading Loading @@ -212,24 +212,21 @@ final class AdditionalSubtypeUtils { * multiple threads are not calling this method at the same time for the same {@code userId}. * </p> * * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter * will be used to return the result. * @param userId The user ID to be associated with. * @return {@link AdditionalSubtypeMap} that contains the additional {@link InputMethodSubtype}. */ static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes, @UserIdInt int userId) { allSubtypes.clear(); static AdditionalSubtypeMap load(@UserIdInt int userId) { final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId)); // Not having the file means there is no additional subtype. if (subtypesFile.exists()) { loadFromFile(allSubtypes, subtypesFile); return loadFromFile(subtypesFile); } return AdditionalSubtypeMap.EMPTY_MAP; } @VisibleForTesting static void loadFromFile(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes, AtomicFile subtypesFile) { static AdditionalSubtypeMap loadFromFile(AtomicFile subtypesFile) { final ArrayMap<String, List<InputMethodSubtype>> allSubtypes = new ArrayMap<>(); try (FileInputStream fis = subtypesFile.openRead()) { final TypedXmlPullParser parser = Xml.resolvePullParser(fis); int type = parser.next(); Loading Loading @@ -310,5 +307,6 @@ final class AdditionalSubtypeUtils { } catch (XmlPullParserException | IOException | NumberFormatException e) { Slog.w(TAG, "Error reading subtypes", e); } return AdditionalSubtypeMap.of(allSubtypes); } } services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +38 −30 Original line number Diff line number Diff line Loading @@ -286,8 +286,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputManagerInternal mInputManagerInternal; final ImePlatformCompatUtils mImePlatformCompatUtils; final InputMethodDeviceConfigs mInputMethodDeviceConfigs; private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap = new ArrayMap<>(); @GuardedBy("ImfLock.class") @NonNull private AdditionalSubtypeMap mAdditionalSubtypeMap = AdditionalSubtypeMap.EMPTY_MAP; private final UserManagerInternal mUserManagerInternal; private final InputMethodMenuController mMenuController; @NonNull private final InputMethodBindingController mBindingController; Loading Loading @@ -1332,16 +1334,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { synchronized (ImfLock.class) { boolean changed = false; // Note that one package may implement multiple IMEs. final ArrayList<String> changedImes = new ArrayList<>(); for (InputMethodInfo imi : mSettings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { mAdditionalSubtypeMap.remove(imi.getId()); changed = true; changedImes.add(imi.getId()); } } if (changed) { final AdditionalSubtypeMap newMap = mAdditionalSubtypeMap.cloneWithRemoveOrSelf(changedImes); if (newMap != mAdditionalSubtypeMap) { mAdditionalSubtypeMap = newMap; AdditionalSubtypeUtils.save( mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); } if (!changedImes.isEmpty()) { mChangedPackages.add(packageName); } } Loading Loading @@ -1413,7 +1420,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.i(TAG, "Input method reinstalling, clearing additional subtypes: " + imi.getComponent()); mAdditionalSubtypeMap.remove(imi.getId()); mAdditionalSubtypeMap = mAdditionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); } Loading Loading @@ -1648,7 +1656,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // mSettings should be created before buildInputMethodListLocked mSettings = InputMethodSettings.createEmptyMap(userId); AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(userId); mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(context, mSettings.getMethodMap(), userId); Loading Loading @@ -1783,7 +1791,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mSettings = InputMethodSettings.createEmptyMap(newUserId); // Additional subtypes should be reset when the user is changed AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId); mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(newUserId); final String defaultImiId = mSettings.getSelectedInputMethod(); if (DEBUG) { Loading Loading @@ -2016,9 +2024,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && directBootAwareness == DirectBootAwareness.AUTO) { settings = mSettings; } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, directBootAwareness); } Loading Loading @@ -4218,10 +4224,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (mSettings.getUserId() == userId) { if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) { final var newAdditionalSubtypeMap = mSettings.getNewAdditionalSubtypeMap( imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, callingUid); if (mAdditionalSubtypeMap == newAdditionalSubtypeMap) { return; } AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); mAdditionalSubtypeMap = newAdditionalSubtypeMap; final long ident = Binder.clearCallingIdentity(); try { buildInputMethodListLocked(false /* resetDefaultEnabledIme */); Loading @@ -4231,13 +4242,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap( imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); if (additionalSubtypeMap != newAdditionalSubtypeMap) { AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, settings.getMethodMap(), settings.getUserId()); } } } Loading Loading @@ -5072,7 +5085,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull static InputMethodSettings queryInputMethodServicesInternal(Context context, @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @UserIdInt int userId, @NonNull AdditionalSubtypeMap additionalSubtypeMap, @DirectBootAwareness int directBootAwareness) { final Context userAwareContext = context.getUserId() == userId ? context Loading Loading @@ -5112,7 +5125,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull static InputMethodMap filterInputMethodServices( ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @NonNull AdditionalSubtypeMap additionalSubtypeMap, List<String> enabledInputMethodList, Context userAwareContext, List<ResolveInfo> services) { final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>(); Loading Loading @@ -5512,9 +5525,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (userId == mSettings.getUserId()) { settings = mSettings; } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); } Loading @@ -5522,9 +5533,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); } Loading Loading @@ -6572,9 +6581,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub nextIme = mSettings.getSelectedInputMethod(); nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); final InputMethodSettings settings = queryInputMethodServicesInternal( mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); Loading services/core/java/com/android/server/inputmethod/InputMethodSettings.java +9 −9 Original line number Diff line number Diff line Loading @@ -24,7 +24,6 @@ import android.content.pm.PackageManagerInternal; import android.os.LocaleList; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.util.Printer; Loading Loading @@ -614,26 +613,27 @@ final class InputMethodSettings { explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } boolean setAdditionalInputMethodSubtypes(@NonNull String imeId, @NonNull AdditionalSubtypeMap getNewAdditionalSubtypeMap(@NonNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @NonNull AdditionalSubtypeMap additionalSubtypeMap, @NonNull PackageManagerInternal packageManagerInternal, int callingUid) { final InputMethodInfo imi = mMethodMap.get(imeId); if (imi == null) { return false; return additionalSubtypeMap; } if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid, imi.getPackageName())) { return false; return additionalSubtypeMap; } final AdditionalSubtypeMap newMap; if (subtypes.isEmpty()) { additionalSubtypeMap.remove(imi.getId()); newMap = additionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId()); } else { additionalSubtypeMap.put(imi.getId(), subtypes); newMap = additionalSubtypeMap.cloneWithPut(imi.getId(), subtypes); } AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId()); return true; return newMap; } boolean setEnabledInputMethodSubtypes(@NonNull String imeId, Loading services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java 0 → 100644 +125 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.assertThat; import android.util.ArrayMap; import android.view.inputmethod.InputMethodSubtype; import androidx.annotation.NonNull; import org.junit.Test; import java.util.List; public final class AdditionalSubtypeMapTest { private static final String TEST_IME1_ID = "com.android.test.inputmethod/.TestIme1"; private static final String TEST_IME2_ID = "com.android.test.inputmethod/.TestIme2"; private static InputMethodSubtype createTestSubtype(String locale) { return new InputMethodSubtype .InputMethodSubtypeBuilder() .setSubtypeNameResId(0) .setSubtypeIconResId(0) .setSubtypeLocale(locale) .setIsAsciiCapable(true) .build(); } private static final InputMethodSubtype TEST_SUBTYPE_EN_US = createTestSubtype("en_US"); private static final InputMethodSubtype TEST_SUBTYPE_JA_JP = createTestSubtype("ja_JP"); private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST1 = List.of(TEST_SUBTYPE_EN_US); private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST2 = List.of(TEST_SUBTYPE_JA_JP); private static ArrayMap<String, List<InputMethodSubtype>> mapOf( @NonNull String key1, @NonNull List<InputMethodSubtype> value1) { final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>(); map.put(key1, value1); return map; } private static ArrayMap<String, List<InputMethodSubtype>> mapOf( @NonNull String key1, @NonNull List<InputMethodSubtype> value1, @NonNull String key2, @NonNull List<InputMethodSubtype> value2) { final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>(); map.put(key1, value1); map.put(key2, value2); return map; } @Test public void testOfReturnsEmptyInstance() { assertThat(AdditionalSubtypeMap.of(new ArrayMap<>())) .isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); } @Test public void testOfReturnsNewInstance() { final AdditionalSubtypeMap instance = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); assertThat(instance.keySet()).containsExactly(TEST_IME1_ID); assertThat(instance.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); } @Test public void testCloneWithRemoveOrSelfReturnsEmptyInstance() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID); assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); } @Test public void testCloneWithRemoveOrSelfWithMultipleKeysReturnsEmptyInstance() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf( List.of(TEST_IME1_ID, TEST_IME2_ID)); assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); } @Test public void testCloneWithRemoveOrSelfReturnsNewInstance() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID); assertThat(result.keySet()).containsExactly(TEST_IME2_ID); assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2); } @Test public void testCloneWithPutWithNewKey() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST2); assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID); assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2); } @Test public void testCloneWithPutWithExistingKey() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST1); assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID); assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); } } Loading
services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java 0 → 100644 +155 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.ArrayMap; import android.view.inputmethod.InputMethodSubtype; import java.util.Collection; import java.util.List; /** * An on-memory immutable data representation of subtype.xml, which contains so-called additional * {@link InputMethodSubtype}. * * <p>While the data structure could be also used for general purpose map from IME ID to * a list of {@link InputMethodSubtype}, unlike {@link InputMethodMap} this particular data * structure is currently used only around additional {@link InputMethodSubtype}, which is why this * class is (still) called {@code AdditionalSubtypeMap} rather than {@code InputMethodSubtypeMap}. * </p> */ final class AdditionalSubtypeMap { /** * An empty {@link AdditionalSubtypeMap}. */ static final AdditionalSubtypeMap EMPTY_MAP = new AdditionalSubtypeMap(new ArrayMap<>()); @NonNull private final ArrayMap<String, List<InputMethodSubtype>> mMap; @AnyThread @NonNull private static AdditionalSubtypeMap createOrEmpty( @NonNull ArrayMap<String, List<InputMethodSubtype>> map) { return map.isEmpty() ? EMPTY_MAP : new AdditionalSubtypeMap(map); } /** * Create a new instance from the given {@link ArrayMap}. * * <p>This method effectively creates a new copy of map.</p> * * @param map An {@link ArrayMap} from which {@link AdditionalSubtypeMap} is to be created. * @return A {@link AdditionalSubtypeMap} that contains a new copy of {@code map}. */ @AnyThread @NonNull static AdditionalSubtypeMap of(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) { return createOrEmpty(map); } /** * Create a new instance of {@link AdditionalSubtypeMap} from an existing * {@link AdditionalSubtypeMap} by removing {@code key}, or return {@code map} itself if it does * not contain an entry of {@code key}. * * @param key The key to be removed from {@code map}. * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain * {@code key}, or {@code map} itself if it does not contain an entry of {@code key}. */ @AnyThread @NonNull AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull String key) { if (isEmpty() || !containsKey(key)) { return this; } final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); newMap.remove(key); return createOrEmpty(newMap); } /** * Create a new instance of {@link AdditionalSubtypeMap} from an existing * {@link AdditionalSubtypeMap} by removing {@code keys} or return {@code map} itself if it does * not contain any entry for {@code keys}. * * @param keys Keys to be removed from {@code map}. * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain * {@code keys}, or {@code map} itself if it does not contain any entry of {@code keys}. */ @AnyThread @NonNull AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull Collection<String> keys) { if (isEmpty()) { return this; } final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); return newMap.removeAll(keys) ? createOrEmpty(newMap) : this; } /** * Create a new instance of {@link AdditionalSubtypeMap} from an existing * {@link AdditionalSubtypeMap} by putting {@code key} and {@code value}. * * @param key Key to be put into {@code map}. * @param value Value to be put into {@code map}. * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to contain the * pair of {@code key} and {@code value}. */ @AnyThread @NonNull AdditionalSubtypeMap cloneWithPut( @Nullable String key, @NonNull List<InputMethodSubtype> value) { final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); newMap.put(key, value); return new AdditionalSubtypeMap(newMap); } private AdditionalSubtypeMap(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) { mMap = map; } @AnyThread @Nullable List<InputMethodSubtype> get(@Nullable String key) { return mMap.get(key); } @AnyThread boolean containsKey(@Nullable String key) { return mMap.containsKey(key); } @AnyThread boolean isEmpty() { return mMap.isEmpty(); } @AnyThread @NonNull Collection<String> keySet() { return mMap.keySet(); } @AnyThread int size() { return mMap.size(); } }
services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +12 −14 Original line number Diff line number Diff line Loading @@ -108,12 +108,12 @@ final class AdditionalSubtypeUtils { * multiple threads are not calling this method at the same time for the same {@code userId}. * </p> * * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty * map deletes the file. * @param allSubtypes {@link AdditionalSubtypeMap} from IME ID to additional subtype list. * Passing an empty map deletes the file. * @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}. * @param userId The user ID to be associated with. */ static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, static void save(AdditionalSubtypeMap allSubtypes, InputMethodMap methodMap, @UserIdInt int userId) { final File inputMethodDir = getInputMethodDir(userId); Loading Loading @@ -142,7 +142,7 @@ final class AdditionalSubtypeUtils { } @VisibleForTesting static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, static void saveToFile(AdditionalSubtypeMap allSubtypes, InputMethodMap methodMap, AtomicFile subtypesFile) { // Safety net for the case that this function is called before methodMap is set. final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; Loading Loading @@ -212,24 +212,21 @@ final class AdditionalSubtypeUtils { * multiple threads are not calling this method at the same time for the same {@code userId}. * </p> * * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter * will be used to return the result. * @param userId The user ID to be associated with. * @return {@link AdditionalSubtypeMap} that contains the additional {@link InputMethodSubtype}. */ static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes, @UserIdInt int userId) { allSubtypes.clear(); static AdditionalSubtypeMap load(@UserIdInt int userId) { final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId)); // Not having the file means there is no additional subtype. if (subtypesFile.exists()) { loadFromFile(allSubtypes, subtypesFile); return loadFromFile(subtypesFile); } return AdditionalSubtypeMap.EMPTY_MAP; } @VisibleForTesting static void loadFromFile(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes, AtomicFile subtypesFile) { static AdditionalSubtypeMap loadFromFile(AtomicFile subtypesFile) { final ArrayMap<String, List<InputMethodSubtype>> allSubtypes = new ArrayMap<>(); try (FileInputStream fis = subtypesFile.openRead()) { final TypedXmlPullParser parser = Xml.resolvePullParser(fis); int type = parser.next(); Loading Loading @@ -310,5 +307,6 @@ final class AdditionalSubtypeUtils { } catch (XmlPullParserException | IOException | NumberFormatException e) { Slog.w(TAG, "Error reading subtypes", e); } return AdditionalSubtypeMap.of(allSubtypes); } }
services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +38 −30 Original line number Diff line number Diff line Loading @@ -286,8 +286,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputManagerInternal mInputManagerInternal; final ImePlatformCompatUtils mImePlatformCompatUtils; final InputMethodDeviceConfigs mInputMethodDeviceConfigs; private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap = new ArrayMap<>(); @GuardedBy("ImfLock.class") @NonNull private AdditionalSubtypeMap mAdditionalSubtypeMap = AdditionalSubtypeMap.EMPTY_MAP; private final UserManagerInternal mUserManagerInternal; private final InputMethodMenuController mMenuController; @NonNull private final InputMethodBindingController mBindingController; Loading Loading @@ -1332,16 +1334,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { synchronized (ImfLock.class) { boolean changed = false; // Note that one package may implement multiple IMEs. final ArrayList<String> changedImes = new ArrayList<>(); for (InputMethodInfo imi : mSettings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { mAdditionalSubtypeMap.remove(imi.getId()); changed = true; changedImes.add(imi.getId()); } } if (changed) { final AdditionalSubtypeMap newMap = mAdditionalSubtypeMap.cloneWithRemoveOrSelf(changedImes); if (newMap != mAdditionalSubtypeMap) { mAdditionalSubtypeMap = newMap; AdditionalSubtypeUtils.save( mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); } if (!changedImes.isEmpty()) { mChangedPackages.add(packageName); } } Loading Loading @@ -1413,7 +1420,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.i(TAG, "Input method reinstalling, clearing additional subtypes: " + imi.getComponent()); mAdditionalSubtypeMap.remove(imi.getId()); mAdditionalSubtypeMap = mAdditionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); } Loading Loading @@ -1648,7 +1656,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // mSettings should be created before buildInputMethodListLocked mSettings = InputMethodSettings.createEmptyMap(userId); AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(userId); mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(context, mSettings.getMethodMap(), userId); Loading Loading @@ -1783,7 +1791,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mSettings = InputMethodSettings.createEmptyMap(newUserId); // Additional subtypes should be reset when the user is changed AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId); mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(newUserId); final String defaultImiId = mSettings.getSelectedInputMethod(); if (DEBUG) { Loading Loading @@ -2016,9 +2024,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && directBootAwareness == DirectBootAwareness.AUTO) { settings = mSettings; } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, directBootAwareness); } Loading Loading @@ -4218,10 +4224,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (mSettings.getUserId() == userId) { if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) { final var newAdditionalSubtypeMap = mSettings.getNewAdditionalSubtypeMap( imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, callingUid); if (mAdditionalSubtypeMap == newAdditionalSubtypeMap) { return; } AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); mAdditionalSubtypeMap = newAdditionalSubtypeMap; final long ident = Binder.clearCallingIdentity(); try { buildInputMethodListLocked(false /* resetDefaultEnabledIme */); Loading @@ -4231,13 +4242,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap( imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); if (additionalSubtypeMap != newAdditionalSubtypeMap) { AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, settings.getMethodMap(), settings.getUserId()); } } } Loading Loading @@ -5072,7 +5085,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull static InputMethodSettings queryInputMethodServicesInternal(Context context, @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @UserIdInt int userId, @NonNull AdditionalSubtypeMap additionalSubtypeMap, @DirectBootAwareness int directBootAwareness) { final Context userAwareContext = context.getUserId() == userId ? context Loading Loading @@ -5112,7 +5125,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull static InputMethodMap filterInputMethodServices( ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @NonNull AdditionalSubtypeMap additionalSubtypeMap, List<String> enabledInputMethodList, Context userAwareContext, List<ResolveInfo> services) { final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>(); Loading Loading @@ -5512,9 +5525,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (userId == mSettings.getUserId()) { settings = mSettings; } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); } Loading @@ -5522,9 +5533,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); } Loading Loading @@ -6572,9 +6581,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub nextIme = mSettings.getSelectedInputMethod(); nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); final InputMethodSettings settings = queryInputMethodServicesInternal( mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); Loading
services/core/java/com/android/server/inputmethod/InputMethodSettings.java +9 −9 Original line number Diff line number Diff line Loading @@ -24,7 +24,6 @@ import android.content.pm.PackageManagerInternal; import android.os.LocaleList; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.util.Printer; Loading Loading @@ -614,26 +613,27 @@ final class InputMethodSettings { explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } boolean setAdditionalInputMethodSubtypes(@NonNull String imeId, @NonNull AdditionalSubtypeMap getNewAdditionalSubtypeMap(@NonNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @NonNull AdditionalSubtypeMap additionalSubtypeMap, @NonNull PackageManagerInternal packageManagerInternal, int callingUid) { final InputMethodInfo imi = mMethodMap.get(imeId); if (imi == null) { return false; return additionalSubtypeMap; } if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid, imi.getPackageName())) { return false; return additionalSubtypeMap; } final AdditionalSubtypeMap newMap; if (subtypes.isEmpty()) { additionalSubtypeMap.remove(imi.getId()); newMap = additionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId()); } else { additionalSubtypeMap.put(imi.getId(), subtypes); newMap = additionalSubtypeMap.cloneWithPut(imi.getId(), subtypes); } AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId()); return true; return newMap; } boolean setEnabledInputMethodSubtypes(@NonNull String imeId, Loading
services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java 0 → 100644 +125 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.assertThat; import android.util.ArrayMap; import android.view.inputmethod.InputMethodSubtype; import androidx.annotation.NonNull; import org.junit.Test; import java.util.List; public final class AdditionalSubtypeMapTest { private static final String TEST_IME1_ID = "com.android.test.inputmethod/.TestIme1"; private static final String TEST_IME2_ID = "com.android.test.inputmethod/.TestIme2"; private static InputMethodSubtype createTestSubtype(String locale) { return new InputMethodSubtype .InputMethodSubtypeBuilder() .setSubtypeNameResId(0) .setSubtypeIconResId(0) .setSubtypeLocale(locale) .setIsAsciiCapable(true) .build(); } private static final InputMethodSubtype TEST_SUBTYPE_EN_US = createTestSubtype("en_US"); private static final InputMethodSubtype TEST_SUBTYPE_JA_JP = createTestSubtype("ja_JP"); private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST1 = List.of(TEST_SUBTYPE_EN_US); private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST2 = List.of(TEST_SUBTYPE_JA_JP); private static ArrayMap<String, List<InputMethodSubtype>> mapOf( @NonNull String key1, @NonNull List<InputMethodSubtype> value1) { final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>(); map.put(key1, value1); return map; } private static ArrayMap<String, List<InputMethodSubtype>> mapOf( @NonNull String key1, @NonNull List<InputMethodSubtype> value1, @NonNull String key2, @NonNull List<InputMethodSubtype> value2) { final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>(); map.put(key1, value1); map.put(key2, value2); return map; } @Test public void testOfReturnsEmptyInstance() { assertThat(AdditionalSubtypeMap.of(new ArrayMap<>())) .isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); } @Test public void testOfReturnsNewInstance() { final AdditionalSubtypeMap instance = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); assertThat(instance.keySet()).containsExactly(TEST_IME1_ID); assertThat(instance.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); } @Test public void testCloneWithRemoveOrSelfReturnsEmptyInstance() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID); assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); } @Test public void testCloneWithRemoveOrSelfWithMultipleKeysReturnsEmptyInstance() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf( List.of(TEST_IME1_ID, TEST_IME2_ID)); assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); } @Test public void testCloneWithRemoveOrSelfReturnsNewInstance() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID); assertThat(result.keySet()).containsExactly(TEST_IME2_ID); assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2); } @Test public void testCloneWithPutWithNewKey() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST2); assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID); assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2); } @Test public void testCloneWithPutWithExistingKey() { final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST1); assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID); assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); } }