summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl19
-rw-r--r--core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java254
-rw-r--r--core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java158
3 files changed, 431 insertions, 0 deletions
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl
new file mode 100644
index 000000000000..18bd6e598756
--- /dev/null
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 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.internal.inputmethod;
+
+parcelable InputMethodSubtypeHandle;
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
new file mode 100644
index 000000000000..780c637b3c9b
--- /dev/null
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2022 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.internal.inputmethod;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils.SimpleStringSplitter;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.security.InvalidParameterException;
+import java.util.Objects;
+
+/**
+ * A stable and serializable identifier for the pair of {@link InputMethodInfo#getId()} and
+ * {@link android.view.inputmethod.InputMethodSubtype}.
+ *
+ * <p>To save {@link InputMethodSubtypeHandle} to storage, call {@link #toStringHandle()} to get a
+ * {@link String} handle and just save it. Once you load a {@link String} handle, you can obtain a
+ * {@link InputMethodSubtypeHandle} instance from {@link #of(String)}.</p>
+ *
+ * <p>For better readability, consider specifying {@link RawHandle} annotation to {@link String}
+ * object when it is a raw {@link String} handle.</p>
+ */
+public final class InputMethodSubtypeHandle implements Parcelable {
+ private static final String SUBTYPE_TAG = "subtype";
+ private static final char DATA_SEPARATOR = ':';
+
+ /**
+ * Can be used to annotate {@link String} object if it is raw handle format.
+ */
+ @Retention(SOURCE)
+ @Target({ElementType.METHOD, ElementType.FIELD, ElementType.LOCAL_VARIABLE,
+ ElementType.PARAMETER})
+ public @interface RawHandle {
+ }
+
+ /**
+ * The main content of this {@link InputMethodSubtypeHandle}. Is designed to be safe to be
+ * saved into storage.
+ */
+ @RawHandle
+ private final String mHandle;
+
+ /**
+ * Encode {@link InputMethodInfo} and {@link InputMethodSubtype#hashCode()} into
+ * {@link RawHandle}.
+ *
+ * @param imeId {@link InputMethodInfo#getId()} to be used.
+ * @param subtypeHashCode {@link InputMethodSubtype#hashCode()} to be used.
+ * @return The encoded {@link RawHandle} string.
+ */
+ @AnyThread
+ @RawHandle
+ @NonNull
+ private static String encodeHandle(@NonNull String imeId, int subtypeHashCode) {
+ return imeId + DATA_SEPARATOR + SUBTYPE_TAG + DATA_SEPARATOR + subtypeHashCode;
+ }
+
+ private InputMethodSubtypeHandle(@NonNull String handle) {
+ mHandle = handle;
+ }
+
+ /**
+ * Creates {@link InputMethodSubtypeHandle} from {@link InputMethodInfo} and
+ * {@link InputMethodSubtype}.
+ *
+ * @param imi {@link InputMethodInfo} to be used.
+ * @param subtype {@link InputMethodSubtype} to be used.
+ * @return A {@link InputMethodSubtypeHandle} object.
+ */
+ @AnyThread
+ @NonNull
+ public static InputMethodSubtypeHandle of(
+ @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+ final int subtypeHashCode =
+ subtype != null ? subtype.hashCode() : InputMethodSubtype.SUBTYPE_ID_NONE;
+ return new InputMethodSubtypeHandle(encodeHandle(imi.getId(), subtypeHashCode));
+ }
+
+ /**
+ * Creates {@link InputMethodSubtypeHandle} from a {@link RawHandle} {@link String}, which can
+ * be obtained by {@link #toStringHandle()}.
+ *
+ * @param stringHandle {@link RawHandle} {@link String} to be parsed.
+ * @return A {@link InputMethodSubtypeHandle} object.
+ * @throws NullPointerException when {@code stringHandle} is {@code null}
+ * @throws InvalidParameterException when {@code stringHandle} is not a valid {@link RawHandle}.
+ */
+ @AnyThread
+ @NonNull
+ public static InputMethodSubtypeHandle of(@RawHandle @NonNull String stringHandle) {
+ final SimpleStringSplitter splitter = new SimpleStringSplitter(DATA_SEPARATOR);
+ splitter.setString(Objects.requireNonNull(stringHandle));
+ if (!splitter.hasNext()) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ final String imeId = splitter.next();
+ final ComponentName componentName = ComponentName.unflattenFromString(imeId);
+ if (componentName == null) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ // TODO: Consolidate IME ID validation logic into one place.
+ if (!Objects.equals(componentName.flattenToShortString(), imeId)) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ if (!splitter.hasNext()) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ final String source = splitter.next();
+ if (!Objects.equals(source, SUBTYPE_TAG)) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ if (!splitter.hasNext()) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ final String hashCodeStr = splitter.next();
+ if (splitter.hasNext()) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+ final int subtypeHashCode;
+ try {
+ subtypeHashCode = Integer.parseInt(hashCodeStr);
+ } catch (NumberFormatException ignore) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+
+ // Redundant expressions (e.g. "0001" instead of "1") are not allowed.
+ if (!Objects.equals(encodeHandle(imeId, subtypeHashCode), stringHandle)) {
+ throw new InvalidParameterException("Invalid handle=" + stringHandle);
+ }
+
+ return new InputMethodSubtypeHandle(stringHandle);
+ }
+
+ /**
+ * @return {@link ComponentName} of the input method.
+ * @see InputMethodInfo#getComponent()
+ */
+ @AnyThread
+ @NonNull
+ public ComponentName getComponentName() {
+ return ComponentName.unflattenFromString(getImeId());
+ }
+
+ /**
+ * @return IME ID.
+ * @see InputMethodInfo#getId()
+ */
+ @AnyThread
+ @NonNull
+ public String getImeId() {
+ return mHandle.substring(0, mHandle.indexOf(DATA_SEPARATOR));
+ }
+
+ /**
+ * @return {@link RawHandle} {@link String} data that should be stable and persistable.
+ * @see #of(String)
+ */
+ @RawHandle
+ @AnyThread
+ @NonNull
+ public String toStringHandle() {
+ return mHandle;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @AnyThread
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof InputMethodSubtypeHandle)) {
+ return false;
+ }
+ final InputMethodSubtypeHandle that = (InputMethodSubtypeHandle) obj;
+ return Objects.equals(mHandle, that.mHandle);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @AnyThread
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mHandle);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @AnyThread
+ @NonNull
+ @Override
+ public String toString() {
+ return "InputMethodSubtypeHandle{mHandle=" + mHandle + "}";
+ }
+
+ /**
+ * {@link Creator} for parcelable.
+ */
+ public static final Creator<InputMethodSubtypeHandle> CREATOR = new Creator<>() {
+ @Override
+ public InputMethodSubtypeHandle createFromParcel(Parcel in) {
+ return of(in.readString8());
+ }
+
+ @Override
+ public InputMethodSubtypeHandle[] newArray(int size) {
+ return new InputMethodSubtypeHandle[size];
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @AnyThread
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @AnyThread
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(toStringHandle());
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java
new file mode 100644
index 000000000000..f111bf6fcd64
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2022 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.internal.inputmethod;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.InvalidParameterException;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class InputMethodSubtypeHandleTest {
+
+ @Test
+ public void testCreateFromRawHandle() {
+ {
+ final InputMethodSubtypeHandle handle =
+ InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1");
+ assertNotNull(handle);
+ assertEquals("com.android.test/.Ime1:subtype:1", handle.toStringHandle());
+ assertEquals("com.android.test/.Ime1", handle.getImeId());
+ assertEquals(ComponentName.unflattenFromString("com.android.test/.Ime1"),
+ handle.getComponentName());
+ }
+
+ assertThrows(NullPointerException.class, () -> InputMethodSubtypeHandle.of(null));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(""));
+
+ // The IME ID must use ComponentName#flattenToShortString(), not #flattenToString().
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/com.android.test.Ime1:subtype:1"));
+
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:subtype:0001"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:subtype:1!"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:subtype:1:"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:subtype:1:2"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:subtype:a"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:subtype:0x01"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "com.android.test/.Ime1:Subtype:a"));
+ assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+ "ime1:subtype:1"));
+ }
+
+ @Test
+ public void testCreateFromInputMethodInfo() {
+ final InputMethodInfo imi = new InputMethodInfo(
+ "com.android.test", "com.android.test.Ime1", "TestIME", null);
+ {
+ final InputMethodSubtypeHandle handle = InputMethodSubtypeHandle.of(imi, null);
+ assertNotNull(handle);
+ assertEquals("com.android.test/.Ime1:subtype:0", handle.toStringHandle());
+ assertEquals("com.android.test/.Ime1", handle.getImeId());
+ assertEquals(ComponentName.unflattenFromString("com.android.test/.Ime1"),
+ handle.getComponentName());
+ }
+
+ final InputMethodSubtype subtype =
+ new InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(1).build();
+ {
+ final InputMethodSubtypeHandle handle = InputMethodSubtypeHandle.of(imi, subtype);
+ assertNotNull(handle);
+ assertEquals("com.android.test/.Ime1:subtype:1", handle.toStringHandle());
+ assertEquals("com.android.test/.Ime1", handle.getImeId());
+ assertEquals(ComponentName.unflattenFromString("com.android.test/.Ime1"),
+ handle.getComponentName());
+ }
+
+ assertThrows(NullPointerException.class, () -> InputMethodSubtypeHandle.of(null, null));
+ assertThrows(NullPointerException.class, () -> InputMethodSubtypeHandle.of(null, subtype));
+ }
+
+ @Test
+ public void testEquality() {
+ assertEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"),
+ InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"));
+ assertEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1").hashCode(),
+ InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1").hashCode());
+
+ assertNotEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"),
+ InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:2"));
+ assertNotEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"),
+ InputMethodSubtypeHandle.of("com.android.test/.Ime2:subtype:1"));
+ }
+
+ @Test
+ public void testParcelablility() {
+ final InputMethodSubtypeHandle original =
+ InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1");
+ final InputMethodSubtypeHandle cloned = cloneHandle(original);
+ assertEquals(original, cloned);
+ assertEquals(original.hashCode(), cloned.hashCode());
+ assertEquals(original.getComponentName(), cloned.getComponentName());
+ assertEquals(original.getImeId(), cloned.getImeId());
+ assertEquals(original.toStringHandle(), cloned.toStringHandle());
+ }
+
+ @Test
+ public void testNoUnnecessaryStringInstantiationInToStringHandle() {
+ final String validHandleStr = "com.android.test/.Ime1:subtype:1";
+ // Verify that toStringHandle() returns the same String object if the input is valid for
+ // an efficient memory usage.
+ assertSame(validHandleStr, InputMethodSubtypeHandle.of(validHandleStr).toStringHandle());
+ }
+
+ @NonNull
+ private static InputMethodSubtypeHandle cloneHandle(
+ @NonNull InputMethodSubtypeHandle original) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ original.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return InputMethodSubtypeHandle.CREATOR.createFromParcel(parcel);
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ }
+ }
+ }
+}