Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 191ca0b4 authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

Add InputMethodSubtypeHandle (again)

This CL introduces

  InputMethodSubtypeHandle

which is similar to what we used to have in the past [1].

In the previous attempt,

  com.android.server.input.PersistentDataStore

saved the data into the storage in the following format.

  <keyboard-layout
      descriptor="..."
      input-method-id="com.android.testime/.Ime1"
      input-method-subtype-id="1"
      ... />

While the new format is going to be something as follows, in general
InputMethodSubtypeHandle enables you to treat the actual String object
as an opaque byte data by providing both decode/encode methods with
data validation.

  <keyboard-layout
      descriptor="..."
      input-method-subtype-handle="com.android.testime/.Ime1:subtype:1"
      ... />

In addition to decode/encode methods, InputMethodSubtypeHandle
provides several useful functionality such as Parcel support and
methods like getComponentName().

See test cases for details.

 [1]: Ie88ce1ab77dbfe03ab51d89c1dc9e0a7ddbb3216
      d5f7ed9f

Bug: 252816846
Test: atest FrameworksCoreTests:InputMethodSubtypeHandleTest
Change-Id: I5b052fba64f8036e7dd874c34b81f5fd4ffeaca3
parent 7a5652f7
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
/*
 * 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;
+254 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}
+158 −0
Original line number Diff line number Diff line
/*
 * 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();
            }
        }
    }
}