Loading core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +13 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -242,7 +243,12 @@ final class IInputMethodManagerGlobalInvoker { return new ArrayList<>(); } try { return service.getInputMethodList(userId, directBootAwareness); if (Flags.useInputMethodInfoSafeList()) { return InputMethodInfoSafeList.extractFrom( service.getInputMethodList(userId, directBootAwareness)); } else { return service.getInputMethodListLegacy(userId, directBootAwareness); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } Loading @@ -257,7 +263,12 @@ final class IInputMethodManagerGlobalInvoker { return new ArrayList<>(); } try { return service.getEnabledInputMethodList(userId); if (Flags.useInputMethodInfoSafeList()) { return InputMethodInfoSafeList.extractFrom( service.getEnabledInputMethodList(userId)); } else { return service.getEnabledInputMethodListLegacy(userId); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } Loading core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.aidl 0 → 100644 +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; core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java 0 → 100644 +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); } } core/java/com/android/internal/view/IInputMethodManager.aidl +13 −5 Original line number Diff line number Diff line Loading @@ -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 applications. Loading @@ -42,20 +43,27 @@ 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); // TODO(b/339761278): Remove after getInputMethodList() is fully deployed. @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") List<InputMethodInfo> getInputMethodListLegacy(int userId, int directBootAwareness); // TODO(b/339761278): Remove after getEnabledInputMethodList() is fully deployed. @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") List<InputMethodInfo> getEnabledInputMethodListLegacy(int userId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") Loading core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodInfoSafeListTest.java 0 → 100644 +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
core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +13 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -242,7 +243,12 @@ final class IInputMethodManagerGlobalInvoker { return new ArrayList<>(); } try { return service.getInputMethodList(userId, directBootAwareness); if (Flags.useInputMethodInfoSafeList()) { return InputMethodInfoSafeList.extractFrom( service.getInputMethodList(userId, directBootAwareness)); } else { return service.getInputMethodListLegacy(userId, directBootAwareness); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } Loading @@ -257,7 +263,12 @@ final class IInputMethodManagerGlobalInvoker { return new ArrayList<>(); } try { return service.getEnabledInputMethodList(userId); if (Flags.useInputMethodInfoSafeList()) { return InputMethodInfoSafeList.extractFrom( service.getEnabledInputMethodList(userId)); } else { return service.getEnabledInputMethodListLegacy(userId); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } Loading
core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.aidl 0 → 100644 +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;
core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java 0 → 100644 +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); } }
core/java/com/android/internal/view/IInputMethodManager.aidl +13 −5 Original line number Diff line number Diff line Loading @@ -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 applications. Loading @@ -42,20 +43,27 @@ 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); // TODO(b/339761278): Remove after getInputMethodList() is fully deployed. @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") List<InputMethodInfo> getInputMethodListLegacy(int userId, int directBootAwareness); // TODO(b/339761278): Remove after getEnabledInputMethodList() is fully deployed. @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") List<InputMethodInfo> getEnabledInputMethodListLegacy(int userId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") Loading
core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodInfoSafeListTest.java 0 → 100644 +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(); } } } }