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