From 9f560c6ee60f8cfa6571f8c1e41e63ac666b71fa Mon Sep 17 00:00:00 2001 From: Joshua Trask <joshtrask@google.com> Date: Thu, 30 May 2024 15:16:30 +0000 Subject: [PATCH] Introduce `NoOpResolverComparator`. We can use this as a "default" ranker in cases where we already know the desired ranking from the client (subclass/caller) request, as in `NfcResolverActivity`. Test: new unit test Bug: 327665515 Flag: EXEMPT bugfix Change-Id: Ie3a7c1d061c5834559d5a15d1ceca1c12613ce60 --- .../internal/app/NoOpResolverComparator.java | 88 ++++++++++++++ .../internal/app/ResolverActivity.java | 30 +++-- .../app/NoOpResolverComparatorTest.java | 108 ++++++++++++++++++ 3 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 core/java/com/android/internal/app/NoOpResolverComparator.java create mode 100644 core/tests/coretests/src/com/android/internal/app/NoOpResolverComparatorTest.java diff --git a/core/java/com/android/internal/app/NoOpResolverComparator.java b/core/java/com/android/internal/app/NoOpResolverComparator.java new file mode 100644 index 000000000000..51eaa8196d4a --- /dev/null +++ b/core/java/com/android/internal/app/NoOpResolverComparator.java @@ -0,0 +1,88 @@ +/* + * Copyright 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.app; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.Message; +import android.os.UserHandle; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; +import com.android.internal.app.chooser.TargetInfo; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + + +/** + * A basic {@link AbstractResolverComparator} implementation that sorts items into the same order as + * they appeared in the list provided to {@link #doCompute(List)}. "Unknown" items that didn't + * appear in the original list are ordered arbitrarily at the end. + */ +public class NoOpResolverComparator extends AbstractResolverComparator { + @Nullable + private List<ResolveInfo> mOriginalTargetOrder = null; + + public NoOpResolverComparator( + Context launchedFromContext, + Intent intent, + List<UserHandle> targetUserSpaceList) { + super(launchedFromContext, intent, targetUserSpaceList); + } + + @Override + public void doCompute(List<ResolvedComponentInfo> targets) { + mOriginalTargetOrder = new ArrayList<>(); + for (ResolvedComponentInfo target : targets) { + mOriginalTargetOrder.add(target.getResolveInfoAt(0)); + } + afterCompute(); + } + + @Override + public int compare(ResolveInfo lhs, ResolveInfo rhs) { + Comparator<ResolveInfo> c = Comparator.comparingDouble(r -> getScore((ResolveInfo) r)); + c = c.reversed(); + return c.compare(lhs, rhs); + } + + @Override + public float getScore(TargetInfo targetInfo) { + return getScore(targetInfo.getResolveInfo()); + } + + @Override + public void handleResultMessage(Message message) {} + + @VisibleForTesting + public float getScore(ResolveInfo resolveInfo) { + if (!mOriginalTargetOrder.contains(resolveInfo)) { + return 0; + } + + // Assign a score from 1 (for the first item in the original list) down + // to 1/(n+1) for the last item (which is still greater than 0, the + // score we assign to any unknown items). + float rank = mOriginalTargetOrder.indexOf(resolveInfo); + return 1.0f - (rank / (1 + mOriginalTargetOrder.size())); + } +} diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 17adee4cc49e..98d6ec6897a6 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -170,6 +170,10 @@ public class ResolverActivity extends Activity implements // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity. private final boolean mIsIntentPicker; + // Whether this activity was instantiated with a specialized constructor that predefines a list + // of resolutions to be displayed for the target intent (as in, e.g., the NFC use case). + private boolean mHasSubclassSpecifiedResolutions; + // Whether or not this activity supports choosing a default handler for the intent. @VisibleForTesting protected boolean mSupportsAlwaysUseOption; @@ -421,6 +425,8 @@ public class ResolverActivity extends Activity implements setTheme(appliedThemeResId()); super.onCreate(savedInstanceState); + mHasSubclassSpecifiedResolutions = (rList != null); + mQuietModeManager = createQuietModeManager(); // Determine whether we should show that intent is forwarded @@ -1698,17 +1704,25 @@ public class ResolverActivity extends Activity implements isAudioCaptureDevice, initialIntentsUserSpace); } + private AbstractResolverComparator makeResolverComparator(UserHandle userHandle) { + if (mHasSubclassSpecifiedResolutions) { + return new NoOpResolverComparator( + this, getTargetIntent(), getResolverRankerServiceUserHandleList(userHandle)); + } else { + return new ResolverRankerServiceResolverComparator( + this, + getTargetIntent(), + getReferrerPackageName(), + null, + null, + getResolverRankerServiceUserHandleList(userHandle)); + } + } + @VisibleForTesting protected ResolverListController createListController(UserHandle userHandle) { UserHandle queryIntentsUser = getQueryIntentsUser(userHandle); - ResolverRankerServiceResolverComparator resolverComparator = - new ResolverRankerServiceResolverComparator( - this, - getTargetIntent(), - getReferrerPackageName(), - null, - null, - getResolverRankerServiceUserHandleList(userHandle)); + AbstractResolverComparator resolverComparator = makeResolverComparator(userHandle); return new ResolverListController( this, mPm, diff --git a/core/tests/coretests/src/com/android/internal/app/NoOpResolverComparatorTest.java b/core/tests/coretests/src/com/android/internal/app/NoOpResolverComparatorTest.java new file mode 100644 index 000000000000..22c319cd4d08 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/NoOpResolverComparatorTest.java @@ -0,0 +1,108 @@ +/* + * 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.app; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Intent; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Unit tests for the behavior of {@link NoOpResolverComparator}. */ +@RunWith(AndroidJUnit4.class) +public class NoOpResolverComparatorTest { + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + + public final ResolvedComponentInfo resolution1 = + ResolverDataProvider.createResolvedComponentInfo(1, PERSONAL_USER_HANDLE); + public final ResolvedComponentInfo resolution2 = + ResolverDataProvider.createResolvedComponentInfo(2, PERSONAL_USER_HANDLE); + public final ResolvedComponentInfo resolution3 = + ResolverDataProvider.createResolvedComponentInfo(3, PERSONAL_USER_HANDLE); + public final ResolvedComponentInfo resolution4 = + ResolverDataProvider.createResolvedComponentInfo(4, PERSONAL_USER_HANDLE); + + private NoOpResolverComparator mComparator; + + @Before + public void setUp() { + mComparator = new NoOpResolverComparator( + InstrumentationRegistry.getInstrumentation().getTargetContext(), + new Intent(), + List.of(PERSONAL_USER_HANDLE)); + } + + @Test + public void testKnownItemsSortInOriginalOrder() { + List<ResolvedComponentInfo> originalOrder = List.of(resolution1, resolution2, resolution3); + mComparator.doCompute(originalOrder); + + List<ResolvedComponentInfo> queryOrder = new ArrayList<>( + List.of(resolution2, resolution3, resolution1)); + + Collections.sort(queryOrder, mComparator); + assertThat(queryOrder).isEqualTo(originalOrder); + } + + @Test + public void testUnknownItemsSortAfterKnownItems() { + List<ResolvedComponentInfo> originalOrder = List.of(resolution1, resolution2); + mComparator.doCompute(originalOrder); + + // Query includes the unknown `resolution4`. + List<ResolvedComponentInfo> queryOrder = new ArrayList<>( + List.of(resolution2, resolution4, resolution1)); + Collections.sort(queryOrder, mComparator); + + assertThat(queryOrder).isEqualTo(List.of(resolution1, resolution2, resolution4)); + } + + @Test + public void testKnownItemsGetNonZeroScoresInOrder() { + List<ResolvedComponentInfo> originalOrder = List.of(resolution1, resolution2); + mComparator.doCompute(originalOrder); + + float score1 = mComparator.getScore(resolution1.getResolveInfoAt(0)); + float score2 = mComparator.getScore(resolution2.getResolveInfoAt(0)); + + assertThat(score1).isEqualTo(1.0f); + assertThat(score2).isLessThan(score1); + assertThat(score2).isGreaterThan(0.0f); + } + + @Test + public void testUnknownItemsGetZeroScore() { + List<ResolvedComponentInfo> originalOrder = List.of(resolution1, resolution2); + mComparator.doCompute(originalOrder); + + assertThat(mComparator.getScore(resolution3.getResolveInfoAt(0))).isEqualTo(0.0f); + } +} -- GitLab