Introduce InputMethodMap#equals()
This CL enables us to compare two InputMethodMap instances to see
whether they contain exactly the same data or not. Such comparison
would be useful to skip unnecessary operations when there was no
update in the available IMEs.
This CL only adds utility methods, which will be used in subsequent
CLs. Thus there must be no observable behavior change.
Bug: 329703038
Test: atest FrameworksInputMethodSystemServerTests:InputMethodInfoUtilsTest
Test: atest FrameworksInputMethodSystemServerTests:InputMethodMapTest
Change-Id: I944f93a3fd45cfa6367ca396cf0cc45efd29c0e2
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
index 6339686..458c359 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.os.Parcel;
import android.text.TextUtils;
import android.util.Slog;
import android.view.inputmethod.InputMethodInfo;
@@ -323,4 +324,24 @@
return SubtypeUtils.containsSubtypeOf(imi, requiredLocale, checkCountry,
requiredSubtypeMode);
}
+
+ /**
+ * Marshals the given {@link InputMethodInfo} into a byte array.
+ *
+ * @param imi {@link InputMethodInfo} to be marshalled
+ * @return a byte array where the given {@link InputMethodInfo} is marshalled
+ */
+ @NonNull
+ static byte[] marshal(@NonNull InputMethodInfo imi) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ parcel.writeTypedObject(imi, 0);
+ return parcel.marshall();
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ }
+ }
+ }
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMap.java b/services/core/java/com/android/server/inputmethod/InputMethodMap.java
index 221309e..f06643df 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMap.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMap.java
@@ -23,6 +23,7 @@
import android.util.ArrayMap;
import android.view.inputmethod.InputMethodInfo;
+import java.util.Arrays;
import java.util.List;
/**
@@ -99,4 +100,37 @@
}
return updated ? InputMethodMap.of(newMethodMap) : this;
}
+
+ /**
+ * Compares the given two {@link InputMethodMap} instances to see if they contain the same data
+ * or not.
+ *
+ * @param map1 {@link InputMethodMap} to be compared with
+ * @param map2 {@link InputMethodMap} to be compared with
+ * @return {@code true} if both {@link InputMethodMap} instances contain exactly the same data
+ */
+ @AnyThread
+ static boolean equals(@NonNull InputMethodMap map1, @NonNull InputMethodMap map2) {
+ if (map1 == map2) {
+ return true;
+ }
+ final int size = map1.size();
+ if (size != map2.size()) {
+ return false;
+ }
+ for (int i = 0; i < size; ++i) {
+ final var imi1 = map1.valueAt(i);
+ final var imeId = imi1.getId();
+ final var imi2 = map2.get(imeId);
+ if (imi2 == null) {
+ return false;
+ }
+ final var marshaled1 = InputMethodInfoUtils.marshal(imi1);
+ final var marshaled2 = InputMethodInfoUtils.marshal(imi2);
+ if (!Arrays.equals(marshaled1, marshaled2)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodInfoUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodInfoUtilsTest.java
new file mode 100644
index 0000000..50804da
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodInfoUtilsTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.android.server.inputmethod.TestUtils.TEST_IME_ID1;
+import static com.android.server.inputmethod.TestUtils.TEST_IME_ID2;
+import static com.android.server.inputmethod.TestUtils.createFakeInputMethodInfo;
+import static com.android.server.inputmethod.TestUtils.createFakeSubtypes;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.inputmethod.InputMethodInfo;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+public final class InputMethodInfoUtilsTest {
+
+ @Test
+ public void testMarshalSameObject() {
+ final var imi = createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(3));
+ final byte[] buf = InputMethodInfoUtils.marshal(imi);
+
+ assertArrayEquals("The same value must be returned when called multiple times",
+ buf, InputMethodInfoUtils.marshal(imi));
+ assertArrayEquals("The same value must be returned when called multiple times",
+ buf, InputMethodInfoUtils.marshal(imi));
+ }
+
+ @Test
+ public void testMarshalDifferentObjects() {
+ final var imi1 = createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(3));
+ final var imi2 = createFakeInputMethodInfo(TEST_IME_ID2, createFakeSubtypes(0));
+
+ assertFalse("Different inputs must yield different byte patterns", Arrays.equals(
+ InputMethodInfoUtils.marshal(imi1), InputMethodInfoUtils.marshal(imi2)));
+ }
+
+ @NonNull
+ private static <T> T readTypedObject(byte[] data, @NonNull Parcelable.Creator<T> creator) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ parcel.unmarshall(data, 0, data.length);
+ parcel.setDataPosition(0);
+ return Objects.requireNonNull(parcel.readTypedObject(creator));
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ }
+ }
+ }
+
+ @Test
+ public void testUnmarshalSameObject() {
+ final var imi = createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(3));
+ final var cloned = readTypedObject(InputMethodInfoUtils.marshal(imi),
+ InputMethodInfo.CREATOR);
+ assertEquals(imi.getPackageName(), cloned.getPackageName());
+ assertEquals(imi.getSubtypeCount(), cloned.getSubtypeCount());
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodMapTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodMapTest.java
new file mode 100644
index 0000000..5e3bc56
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodMapTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.android.server.inputmethod.TestUtils.TEST_IME_ID1;
+import static com.android.server.inputmethod.TestUtils.TEST_IME_ID2;
+import static com.android.server.inputmethod.TestUtils.TEST_IME_ID3;
+import static com.android.server.inputmethod.TestUtils.createFakeInputMethodInfo;
+import static com.android.server.inputmethod.TestUtils.createFakeSubtypes;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.util.ArrayMap;
+import android.view.inputmethod.InputMethodInfo;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+
+public final class InputMethodMapTest {
+
+ @NonNull
+ private static InputMethodMap toMap(InputMethodInfo... list) {
+ final ArrayMap<String, InputMethodInfo> map = new ArrayMap<>();
+ for (var imi : list) {
+ map.put(imi.getId(), imi);
+ }
+ return InputMethodMap.of(map);
+ }
+
+ @Test
+ public void testEqualsSameObject() {
+ final var imi1 = createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(0));
+ final var imi2 = createFakeInputMethodInfo(TEST_IME_ID2, createFakeSubtypes(3));
+ final var map = toMap(imi1, imi2);
+ assertTrue("Must return true for the same instance",
+ InputMethodMap.equals(map, map));
+ }
+
+ @Test
+ public void testEqualsEquivalentObject() {
+ final var imi1 = createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(0));
+ final var imi2 = createFakeInputMethodInfo(TEST_IME_ID2, createFakeSubtypes(3));
+ assertTrue("Must return true for the equivalent instances",
+ InputMethodMap.equals(toMap(imi1, imi2), toMap(imi1, imi2)));
+
+ assertTrue("Must return true for the equivalent instances",
+ InputMethodMap.equals(toMap(imi1, imi2), toMap(imi2, imi1)));
+ }
+
+ @Test
+ public void testEqualsDifferentKeys() {
+ final var imi1 = createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(0));
+ final var imi2 = createFakeInputMethodInfo(TEST_IME_ID2, createFakeSubtypes(3));
+ final var imi3 = createFakeInputMethodInfo(TEST_IME_ID3, createFakeSubtypes(3));
+ assertFalse("Must return false if keys are different",
+ InputMethodMap.equals(toMap(imi1), toMap(imi1, imi2)));
+ assertFalse("Must return false if keys are different",
+ InputMethodMap.equals(toMap(imi1, imi2), toMap(imi1)));
+ assertFalse("Must return false if keys are different",
+ InputMethodMap.equals(toMap(imi1, imi2), toMap(imi1, imi3)));
+ }
+
+ @Test
+ public void testEqualsDifferentValues() {
+ final var imi1_without_subtypes =
+ createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(0));
+ final var imi1_with_subtypes =
+ createFakeInputMethodInfo(TEST_IME_ID1, createFakeSubtypes(3));
+ final var imi2 = createFakeInputMethodInfo(TEST_IME_ID2, createFakeSubtypes(3));
+ assertFalse("Must return false if values are different",
+ InputMethodMap.equals(toMap(imi1_without_subtypes), toMap(imi1_with_subtypes)));
+ assertFalse("Must return false if values are different",
+ InputMethodMap.equals(
+ toMap(imi1_without_subtypes, imi2),
+ toMap(imi1_with_subtypes, imi2)));
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/TestUtils.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/TestUtils.java
new file mode 100644
index 0000000..c51ff87f
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/TestUtils.java
@@ -0,0 +1,101 @@
+/*
+ * 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.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+public final class TestUtils {
+ /**
+ * {@link ComponentName} for fake {@link InputMethodInfo}.
+ */
+ @NonNull
+ public static final ComponentName TEST_IME_ID1 = Objects.requireNonNull(
+ ComponentName.unflattenFromString("com.android.test.testime1/.InputMethod"));
+
+ /**
+ * {@link ComponentName} for fake {@link InputMethodInfo}.
+ */
+ @NonNull
+ public static final ComponentName TEST_IME_ID2 = Objects.requireNonNull(
+ ComponentName.unflattenFromString("com.android.test.testime2/.InputMethod"));
+
+ /**
+ * {@link ComponentName} for fake {@link InputMethodInfo}.
+ */
+ @NonNull
+ public static final ComponentName TEST_IME_ID3 = Objects.requireNonNull(
+ ComponentName.unflattenFromString("com.android.test.testime3/.InputMethod"));
+
+ /**
+ * Creates a list of fake {@link InputMethodSubtype} for unit testing for the given number.
+ *
+ * @param count The number of fake {@link InputMethodSubtype} objects
+ * @return The list of fake {@link InputMethodSubtype} objects
+ */
+ @NonNull
+ public static ArrayList<InputMethodSubtype> createFakeSubtypes(int count) {
+ final ArrayList<InputMethodSubtype> subtypes = new ArrayList<>(count);
+ for (int i = 0; i < count; ++i) {
+ subtypes.add(
+ new InputMethodSubtype.InputMethodSubtypeBuilder()
+ .setSubtypeId(i + 0x100)
+ .setLanguageTag("en-US")
+ .setSubtypeNameOverride("TestSubtype" + i)
+ .build());
+ }
+ return subtypes;
+ }
+
+ /**
+ * Creates a fake {@link InputMethodInfo} for unit testing.
+ *
+ * @param componentName {@link ComponentName} of the fake {@link InputMethodInfo}
+ * @param subtypes A list of (fake) {@link InputMethodSubtype}
+ * @return a fake {@link InputMethodInfo} object
+ */
+ @NonNull
+ public static InputMethodInfo createFakeInputMethodInfo(
+ @NonNull ComponentName componentName, @NonNull ArrayList<InputMethodSubtype> subtypes) {
+ final ApplicationInfo ai = new ApplicationInfo();
+ ai.packageName = componentName.getPackageName();
+ ai.enabled = true;
+
+ final ServiceInfo si = new ServiceInfo();
+ si.applicationInfo = ai;
+ si.enabled = true;
+ si.packageName = componentName.getPackageName();
+ si.name = componentName.getClassName();
+ si.exported = true;
+ si.nonLocalizedLabel = "Fake Label";
+
+ final ResolveInfo ri = new ResolveInfo();
+ ri.serviceInfo = si;
+
+ return new InputMethodInfo(ri, false /* isAuxIme */, null /* settingsActivity */,
+ subtypes, 0 /* isDefaultResId */, false /* forceDefault */);
+ }
+}