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

Commit 70b53e25 authored by Hiroki Sato's avatar Hiroki Sato Committed by Nishith Khanna
Browse files

Introduce InputMethodInfoSafeList

With this CL we start using InputMethodInfoSafeList to transfer
List<InputMethodInfo> over Binder IPC so that we do not need to worry
about TransactionTooLargeException any more. The new behavior is fully
flag-guarded.

This should enable us to remove our past workarounds [1][2] in the
future.

 [1]: Ibb2940fcc02f3b3b51ba6bbe127d646fd7de7c45
      f0656956
 [2]: I51703563414192ee778f30ab57390da1c1a5ded5
      eed10082

Note on backport:
This change was initially added with a flag guard, but for backporting,
the flag guard is removed and change is directly applied.

Bug: 339761278
Bug: 449416164
Bug: 449181366
Bug: 449393786
Bug: 449227003
Test: atest FrameworksInputMethodSystemServerTests:InputMethodInfoSafeListTest
(cherry picked from commit 93d66878)
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:342d0771f6f8cbca4152d101b5bd671b88fd7213
Merged-In: I0a7667070fcdf17d34b248a5988c38064588718a
Change-Id: I0a7667070fcdf17d34b248a5988c38064588718a
parent f058a484
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import com.android.internal.inputmethod.IInputMethodClient;
import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
import com.android.internal.inputmethod.IRemoteInputConnection;
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.InputMethodInfoSafeList;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.inputmethod.StartInputFlags;
import com.android.internal.inputmethod.StartInputReason;
@@ -242,7 +243,8 @@ final class IInputMethodManagerGlobalInvoker {
            return new ArrayList<>();
        }
        try {
            return service.getInputMethodList(userId, directBootAwareness);
            return InputMethodInfoSafeList.extractFrom(
                    service.getInputMethodList(userId, directBootAwareness));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -257,7 +259,8 @@ final class IInputMethodManagerGlobalInvoker {
            return new ArrayList<>();
        }
        try {
            return service.getEnabledInputMethodList(userId);
            return InputMethodInfoSafeList.extractFrom(
                    service.getEnabledInputMethodList(userId));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
+19 −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.internal.inputmethod;

parcelable InputMethodInfoSafeList;
+156 −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.internal.inputmethod;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.inputmethod.InputMethodInfo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A {@link Parcelable} container that can holds an arbitrary number of {@link InputMethodInfo}
 * without worrying about {@link android.os.TransactionTooLargeException} when passing across
 * process boundary.
 *
 * @see Parcel#readBlob()
 * @see Parcel#writeBlob(byte[])
 */
public final class InputMethodInfoSafeList implements Parcelable {
    @Nullable
    private byte[] mBuffer;

    /**
     * Instantiates a list of {@link InputMethodInfo} from the given {@link InputMethodInfoSafeList}
     * then clears the internal buffer of {@link InputMethodInfoSafeList}.
     *
     * <p>Note that each {@link InputMethodInfo} item is guaranteed to be a copy of the original
     * {@link InputMethodInfo} object.</p>
     *
     * <p>Any subsequent call will return an empty list.</p>
     *
     * @param from {@link InputMethodInfoSafeList} from which the list of {@link InputMethodInfo}
     *             will be extracted
     * @return list of {@link InputMethodInfo} stored in the given {@link InputMethodInfoSafeList}
     */
    @NonNull
    public static List<InputMethodInfo> extractFrom(@Nullable InputMethodInfoSafeList from) {
        final byte[] buf = from.mBuffer;
        from.mBuffer = null;
        if (buf != null) {
            final InputMethodInfo[] array = unmarshall(buf);
            if (array != null) {
                return new ArrayList<>(Arrays.asList(array));
            }
        }
        return new ArrayList<>();
    }

    @NonNull
    private static InputMethodInfo[] toArray(@Nullable List<InputMethodInfo> original) {
        if (original == null) {
            return new InputMethodInfo[0];
        }
        return original.toArray(new InputMethodInfo[0]);
    }

    @Nullable
    private static byte[] marshall(@NonNull InputMethodInfo[] array) {
        Parcel parcel = null;
        try {
            parcel = Parcel.obtain();
            parcel.writeTypedArray(array, 0);
            return parcel.marshall();
        } finally {
            if (parcel != null) {
                parcel.recycle();
            }
        }
    }

    @Nullable
    private static InputMethodInfo[] unmarshall(byte[] data) {
        Parcel parcel = null;
        try {
            parcel = Parcel.obtain();
            parcel.unmarshall(data, 0, data.length);
            parcel.setDataPosition(0);
            return parcel.createTypedArray(InputMethodInfo.CREATOR);
        } finally {
            if (parcel != null) {
                parcel.recycle();
            }
        }
    }

    private InputMethodInfoSafeList(@Nullable byte[] blob) {
        mBuffer = blob;
    }

    /**
     * Instantiates {@link InputMethodInfoSafeList} from the given list of {@link InputMethodInfo}.
     *
     * @param list list of {@link InputMethodInfo} from which {@link InputMethodInfoSafeList} will
     *             be created
     * @return {@link InputMethodInfoSafeList} that stores the given list of {@link InputMethodInfo}
     */
    @NonNull
    public static InputMethodInfoSafeList create(@Nullable List<InputMethodInfo> list) {
        if (list == null || list.isEmpty()) {
            return empty();
        }
        return new InputMethodInfoSafeList(marshall(toArray(list)));
    }

    /**
     * Creates an empty {@link InputMethodInfoSafeList}.
     *
     * @return {@link InputMethodInfoSafeList} that is empty
     */
    @NonNull
    public static InputMethodInfoSafeList empty() {
        return new InputMethodInfoSafeList(null);
    }

    public static final Creator<InputMethodInfoSafeList> CREATOR = new Creator<>() {
        @Override
        public InputMethodInfoSafeList createFromParcel(Parcel in) {
            return new InputMethodInfoSafeList(in.readBlob());
        }

        @Override
        public InputMethodInfoSafeList[] newArray(int size) {
            return new InputMethodInfoSafeList[size];
        }
    };

    @Override
    public int describeContents() {
        // As long as InputMethodInfo#describeContents() is guaranteed to return 0, we can always
        // return 0 here.
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeBlob(mBuffer);
    }
}
+3 −5
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.internal.inputmethod.IInputMethodClient;
import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
import com.android.internal.inputmethod.IRemoteInputConnection;
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.InputMethodInfoSafeList;

/**
 * Public interface to the global input method manager, used by all client
@@ -40,20 +41,17 @@ interface IInputMethodManager {
    void addClient(in IInputMethodClient client, in IRemoteInputConnection inputmethod,
            int untrustedDisplayId);

    // TODO: Use ParceledListSlice instead
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
    InputMethodInfo getCurrentInputMethodInfoAsUser(int userId);

    // TODO: Use ParceledListSlice instead
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
    List<InputMethodInfo> getInputMethodList(int userId, int directBootAwareness);
    InputMethodInfoSafeList getInputMethodList(int userId, int directBootAwareness);

    // TODO: Use ParceledListSlice instead
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
    List<InputMethodInfo> getEnabledInputMethodList(int userId);
    InputMethodInfoSafeList getEnabledInputMethodList(int userId);

    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
+137 −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.internal.inputmethod;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;

import android.annotation.NonNull;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
import android.view.inputmethod.InputMethodInfo;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public final class InputMethodInfoSafeListTest {

    @NonNull
    private static InputMethodInfo createFakeInputMethodInfo(String packageName, String name) {
        final ResolveInfo ri = new ResolveInfo();
        final ServiceInfo si = new ServiceInfo();
        final ApplicationInfo ai = new ApplicationInfo();
        ai.packageName = packageName;
        ai.enabled = true;
        ai.flags |= ApplicationInfo.FLAG_SYSTEM;
        si.applicationInfo = ai;
        si.enabled = true;
        si.packageName = packageName;
        si.name = name;
        si.exported = true;
        si.nonLocalizedLabel = name;
        ri.serviceInfo = si;
        return new InputMethodInfo(ri, false, "", Collections.emptyList(), 1, false);
    }

    @NonNull
    private static List<InputMethodInfo> createTestInputMethodList() {
        final ArrayList<InputMethodInfo> list = new ArrayList<>();
        list.add(createFakeInputMethodInfo("com.android.test.ime1", "TestIme1"));
        list.add(createFakeInputMethodInfo("com.android.test.ime1", "TestIme2"));
        list.add(createFakeInputMethodInfo("com.android.test.ime2", "TestIme"));
        return list;
    }

    @Test
    public void testCreate() {
        assertNotNull(InputMethodInfoSafeList.create(createTestInputMethodList()));
    }

    @Test
    public void testExtract() {
        assertItemsAfterExtract(createTestInputMethodList(), InputMethodInfoSafeList::create);
    }

    @Test
    public void testExtractAfterParceling() {
        assertItemsAfterExtract(createTestInputMethodList(),
                originals -> cloneViaParcel(InputMethodInfoSafeList.create(originals)));
    }

    @Test
    public void testExtractEmptyList() {
        assertItemsAfterExtract(Collections.emptyList(), InputMethodInfoSafeList::create);
    }

    @Test
    public void testExtractAfterParcelingEmptyList() {
        assertItemsAfterExtract(Collections.emptyList(),
                originals -> cloneViaParcel(InputMethodInfoSafeList.create(originals)));
    }

    private static void assertItemsAfterExtract(@NonNull List<InputMethodInfo> originals,
            @NonNull Function<List<InputMethodInfo>, InputMethodInfoSafeList> factory) {
        final InputMethodInfoSafeList list = factory.apply(originals);
        final List<InputMethodInfo> extracted = InputMethodInfoSafeList.extractFrom(list);
        assertEquals(originals.size(), extracted.size());
        for (int i = 0; i < originals.size(); ++i) {
            assertNotSame("InputMethodInfoSafeList.extractFrom() must clone each instance",
                    originals.get(i), extracted.get(i));
            assertEquals("Verify the cloned instances have the equal value",
                    originals.get(i).getPackageName(), extracted.get(i).getPackageName());
        }

        // Subsequent calls of InputMethodInfoSafeList.extractFrom() return an empty list.
        final List<InputMethodInfo> extracted2 = InputMethodInfoSafeList.extractFrom(list);
        assertTrue(extracted2.isEmpty());
    }

    @NonNull
    private static InputMethodInfoSafeList cloneViaParcel(
            @NonNull InputMethodInfoSafeList original) {
        Parcel parcel = null;
        try {
            parcel = Parcel.obtain();
            original.writeToParcel(parcel, 0);
            parcel.setDataPosition(0);
            final InputMethodInfoSafeList newInstance =
                    InputMethodInfoSafeList.CREATOR.createFromParcel(parcel);
            assertNotNull(newInstance);
            return newInstance;
        } finally {
            if (parcel != null) {
                parcel.recycle();
            }
        }
    }
}
Loading