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

Commit a771d2e6 authored by Hiroki Sato's avatar Hiroki Sato Committed by Nolen Johnson
Browse files

[BACKPORT] 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 d9be701a
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ import android.view.autofill.AutofillManager;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.InputMethodDebug;
import com.android.internal.inputmethod.InputMethodInfoSafeList;
import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry;
import com.android.internal.inputmethod.StartInputFlags;
import com.android.internal.inputmethod.StartInputReason;
@@ -1284,7 +1285,8 @@ public final class InputMethodManager {
            // We intentionally do not use UserHandle.getCallingUserId() here because for system
            // services InputMethodManagerInternal.getInputMethodListAsUser() should be used
            // instead.
            return mService.getInputMethodList(UserHandle.myUserId());
            return InputMethodInfoSafeList.extractFrom(
                    mService.getInputMethodList(UserHandle.myUserId()));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -1300,7 +1302,8 @@ public final class InputMethodManager {
    @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
    public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
        try {
            return mService.getInputMethodList(userId);
            return InputMethodInfoSafeList.extractFrom(
                    mService.getInputMethodList(userId));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -1318,7 +1321,8 @@ public final class InputMethodManager {
            // We intentionally do not use UserHandle.getCallingUserId() here because for system
            // services InputMethodManagerInternal.getEnabledInputMethodListAsUser() should be used
            // instead.
            return mService.getEnabledInputMethodList(UserHandle.myUserId());
            return InputMethodInfoSafeList.extractFrom(
                    mService.getEnabledInputMethodList(UserHandle.myUserId()));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -1334,7 +1338,8 @@ public final class InputMethodManager {
    @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
    public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) {
        try {
            return mService.getEnabledInputMethodList(userId);
            return InputMethodInfoSafeList.extractFrom(
                    mService.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 −4
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.EditorInfo;

import com.android.internal.inputmethod.InputMethodInfoSafeList;
import com.android.internal.view.InputBindResult;
import com.android.internal.view.IInputContext;
import com.android.internal.view.IInputMethodClient;
@@ -33,10 +34,8 @@ interface IInputMethodManager {
    void addClient(in IInputMethodClient client, in IInputContext inputContext,
            int untrustedDisplayId);

    // TODO: Use ParceledListSlice instead
    List<InputMethodInfo> getInputMethodList(int userId);
    // TODO: Use ParceledListSlice instead
    List<InputMethodInfo> getEnabledInputMethodList(int userId);
    InputMethodInfoSafeList getInputMethodList(int userId);
    InputMethodInfoSafeList getEnabledInputMethodList(int userId);
    List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in String imiId,
            boolean allowsImplicitlySelectedSubtypes);
    InputMethodSubtype getLastInputMethodSubtype();
+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