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
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 0000000..51eaa81
--- /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 17adee4..98d6ec6 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -170,6 +170,10 @@
// 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 @@
setTheme(appliedThemeResId());
super.onCreate(savedInstanceState);
+ mHasSubclassSpecifiedResolutions = (rList != null);
+
mQuietModeManager = createQuietModeManager();
// Determine whether we should show that intent is forwarded
@@ -1698,17 +1704,25 @@
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 0000000..22c319c
--- /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);
+ }
+}