Save and restore ShortcutInfoCompat#rank

Also converts ShortcutInfoCompat#rank to ChooserTarget#score in
backwards compatibility mode.

Bug: 140580980
Test: ChooserTargetServiceCompatTest.java
Test: ShortcutInfoCompatSaverTest.java
Test: ShareTarget test app
Change-Id: I1ae5f2f59aec08ed9028f105cb66aedd4a09e07c
diff --git a/sharetarget/build.gradle b/sharetarget/build.gradle
index 6961d0f..6579f81 100644
--- a/sharetarget/build.gradle
+++ b/sharetarget/build.gradle
@@ -25,8 +25,8 @@
 }
 
 dependencies {
-    api("androidx.core:core:1.1.0")
-    implementation("androidx.collection:collection:1.0.0")
+    api("androidx.core:core:1.2.0-beta01")
+    api("androidx.collection:collection:1.0.0")
     api(GUAVA_LISTENABLE_FUTURE)
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
 
diff --git a/sharetarget/integration-tests/testapp/build.gradle b/sharetarget/integration-tests/testapp/build.gradle
index dc418e8..bf9f767 100644
--- a/sharetarget/integration-tests/testapp/build.gradle
+++ b/sharetarget/integration-tests/testapp/build.gradle
@@ -22,8 +22,8 @@
 }
 
 dependencies {
-    api("androidx.core:core:1.1.0")
-    api("androidx.sharetarget:sharetarget:1.0.0-alpha02")
+    api("androidx.core:core:1.2.0-beta01")
+    api(project(":sharetarget"))
     api("androidx.appcompat:appcompat:1.1.0")
     api(CONSTRAINT_LAYOUT, { transitive = true })
 }
diff --git a/sharetarget/integration-tests/testapp/src/main/java/androidx/sharetarget/testapp/MainActivity.java b/sharetarget/integration-tests/testapp/src/main/java/androidx/sharetarget/testapp/MainActivity.java
index 5406c6d..10afb4c 100644
--- a/sharetarget/integration-tests/testapp/src/main/java/androidx/sharetarget/testapp/MainActivity.java
+++ b/sharetarget/integration-tests/testapp/src/main/java/androidx/sharetarget/testapp/MainActivity.java
@@ -82,6 +82,7 @@
                 .setLongLived()
                 .setPerson(new Person.Builder().build())
                 .setCategories(categories1)
+                .setRank(2)
                 .build());
         shortcuts.add(new ShortcutInfoCompat.Builder(this, "Person_Two_ID")
                 .setShortLabel("Person_Two")
@@ -89,7 +90,8 @@
                 .setIntent(intent)
                 .setLongLived()
                 .setPerson(new Person.Builder().build())
-                .setCategories(categories2)
+                .setCategories(categories1)
+                .setRank(1)
                 .build());
 
         ShortcutManagerCompat.addDynamicShortcuts(this, shortcuts);
diff --git a/sharetarget/src/androidTest/java/androidx/sharetarget/ChooserTargetServiceCompatTest.java b/sharetarget/src/androidTest/java/androidx/sharetarget/ChooserTargetServiceCompatTest.java
new file mode 100644
index 0000000..e4d5212
--- /dev/null
+++ b/sharetarget/src/androidTest/java/androidx/sharetarget/ChooserTargetServiceCompatTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019 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 androidx.sharetarget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.service.chooser.ChooserTarget;
+
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.sharetarget.ChooserTargetServiceCompat.ShortcutHolder;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ChooserTargetServiceCompatTest {
+    private Context mContext;
+
+    private IconCompat mTestIcon;
+    private Intent mTestIntent;
+    private ShortcutInfoCompatSaverImpl mShortcutSaver;
+
+    @Before
+    public void setup() throws Exception {
+        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+        mTestIntent = new Intent("TestIntent");
+        mTestIcon = IconCompat.createWithResource(mContext,
+                androidx.sharetarget.test.R.drawable.bmp_test);
+        mShortcutSaver = mock(ShortcutInfoCompatSaverImpl.class);
+        when(mShortcutSaver.getShortcutIcon(any())).thenReturn(mTestIcon);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 23)
+    public void testConvertShortcutstoChooserTargets() {
+        ArrayList<ShortcutHolder> testShortcuts = new ArrayList<>();
+        testShortcuts.add(new ShortcutHolder(
+                new ShortcutInfoCompat.Builder(mContext, "shortcut1")
+                        .setIntent(mTestIntent).setShortLabel("label1").setRank(3).build(),
+                new ComponentName("package1", "class1")));
+        testShortcuts.add(new ShortcutHolder(
+                new ShortcutInfoCompat.Builder(mContext, "shortcut2")
+                        .setIntent(mTestIntent).setShortLabel("label2").setRank(7).build(),
+                new ComponentName("package2", "class2")));
+        testShortcuts.add(new ShortcutHolder(
+                new ShortcutInfoCompat.Builder(mContext, "shortcut3")
+                        .setIntent(mTestIntent).setShortLabel("label3").setRank(1).build(),
+                new ComponentName("package3", "class3")));
+        testShortcuts.add(new ShortcutHolder(
+                new ShortcutInfoCompat.Builder(mContext, "shortcut4")
+                        .setIntent(mTestIntent).setShortLabel("label4").setRank(3).build(),
+                new ComponentName("package4", "class4")));
+
+        // Need to clone to keep the original order for testing.
+        ArrayList<ShortcutHolder> clonedList = (ArrayList<ShortcutHolder>) testShortcuts.clone();
+        List<ChooserTarget> chooserTargets =
+                ChooserTargetServiceCompat.convertShortcutsToChooserTargets(
+                        mShortcutSaver, clonedList);
+
+        int[] expectedOrder = {2, 0, 3, 1};
+        float[] expectedScores = {1.0f, 1.0f - 0.01f, 1.0f - 0.01f, 1.0f - 0.02f};
+
+        assertEquals(testShortcuts.size(), chooserTargets.size());
+        for (int i = 0; i < chooserTargets.size(); i++) {
+            ChooserTarget ct = chooserTargets.get(i);
+            ShortcutInfoCompat si = testShortcuts.get(expectedOrder[i]).getShortcut();
+            ComponentName cn = testShortcuts.get(expectedOrder[i]).getTargetClass();
+
+            assertEquals(si.getId(), ct.getIntentExtras().getString(
+                    ShortcutManagerCompat.EXTRA_SHORTCUT_ID));
+            assertEquals(si.getShortLabel(), ct.getTitle());
+            assertTrue(Math.abs(expectedScores[i] - ct.getScore()) < 0.000001);
+            assertEquals(cn.flattenToString(), ct.getComponentName().flattenToString());
+        }
+    }
+}
diff --git a/sharetarget/src/androidTest/java/androidx/sharetarget/ShortcutInfoCompatSaverTest.java b/sharetarget/src/androidTest/java/androidx/sharetarget/ShortcutInfoCompatSaverTest.java
index c020a09..f3f5672 100644
--- a/sharetarget/src/androidTest/java/androidx/sharetarget/ShortcutInfoCompatSaverTest.java
+++ b/sharetarget/src/androidTest/java/androidx/sharetarget/ShortcutInfoCompatSaverTest.java
@@ -30,6 +30,7 @@
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
 
+import androidx.annotation.NonNull;
 import androidx.core.app.Person;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.content.pm.ShortcutInfoCompatSaver;
@@ -106,13 +107,14 @@
                 .setShortLabel("test short label 1")
                 .setLongLabel("test long label 1")
                 .setDisabledMessage("test disabled message 1")
-                .setLongLived()
+                .setLongLived(true)
                 .setPersons(testPersons)
                 .setCategories(testCategories)
                 .setActivity(new ComponentName("test package", "test class"))
                 .setAlwaysBadged()
                 .setIcon(mTestResourceIcon)
                 .setIntents(testIntents)
+                .setRank(3)
                 .build());
         mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext, ID_SHORTCUT_BITMAP_ICON)
                 .setShortLabel("test short label 2")
@@ -138,6 +140,7 @@
                 .setPerson(testPersons[0])
                 .setCategories(testCategories)
                 .setIntents(testIntents)
+                .setRank(8)
                 .build());
         mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext, "shortcut-no-category")
                 .setShortLabel("test short label 5")
@@ -194,6 +197,7 @@
         assertEquals(expected.getDisabledMessage(), actual.getDisabledMessage());
         assertEquals(expected.getLongLabel(), actual.getLongLabel());
         assertEquals(expected.getShortLabel(), actual.getShortLabel());
+        assertEquals(expected.getRank(), actual.getRank());
 
         if (expected.getActivity() == null) {
             assertNull(actual.getActivity());
@@ -231,7 +235,8 @@
 
     @Test
     public void testGetInstance() {
-        ShortcutInfoCompatSaver saver = ShortcutInfoCompatSaverImpl.getInstance(mContext);
+        ShortcutInfoCompatSaver<ListenableFuture<Void>> saver =
+                ShortcutInfoCompatSaverImpl.getInstance(mContext);
         assertNotNull(saver);
         assertEquals(saver, ShortcutInfoCompatSaverImpl.getInstance(mContext));
     }
@@ -410,7 +415,7 @@
             }
         }, new Executor() {
             @Override
-            public void execute(Runnable command) {
+            public void execute(@NonNull Runnable command) {
                 // Run in the current thread
                 command.run();
             }
diff --git a/sharetarget/src/main/java/androidx/sharetarget/ChooserTargetServiceCompat.java b/sharetarget/src/main/java/androidx/sharetarget/ChooserTargetServiceCompat.java
index f713377..90b91d1 100644
--- a/sharetarget/src/main/java/androidx/sharetarget/ChooserTargetServiceCompat.java
+++ b/sharetarget/src/main/java/androidx/sharetarget/ChooserTargetServiceCompat.java
@@ -26,14 +26,17 @@
 import android.service.chooser.ChooserTargetService;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.content.pm.ShortcutManagerCompat;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -53,7 +56,6 @@
     public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName,
             IntentFilter matchedFilter) {
         Context context = getApplicationContext();
-        ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
 
         // Retrieve share targets
         List<ShareTargetCompat> targets = ShareTargetXmlParser.getShareTargets(context);
@@ -71,59 +73,99 @@
             }
         }
         if (matchedTargets.isEmpty()) {
-            return chooserTargets;
+            return Collections.emptyList();
         }
 
         // Retrieve shortcuts
-        ShortcutInfoCompatSaverImpl shortcutSaver = ShortcutInfoCompatSaverImpl.getInstance(
-                context);
+        ShortcutInfoCompatSaverImpl shortcutSaver =
+                ShortcutInfoCompatSaverImpl.getInstance(context);
         List<ShortcutInfoCompat> shortcuts;
         try {
             shortcuts = shortcutSaver.getShortcuts();
         } catch (Exception e) {
             Log.e(TAG, "Failed to retrieve shortcuts: ", e);
-            return chooserTargets;
+            return Collections.emptyList();
         }
         if (shortcuts == null || shortcuts.isEmpty()) {
-            return chooserTargets;
+            return Collections.emptyList();
         }
 
+        // List of matched shortcuts with their target component names
+        List<ShortcutHolder> matchedShortcuts = new ArrayList<>();
         for (ShortcutInfoCompat shortcut : shortcuts) {
-            ShareTargetCompat target = null;
             for (ShareTargetCompat item : matchedTargets) {
-                // Shortcut must have all share target categories (AND operation)
+                // Shortcut must have all the share target's categories (AND operation)
                 if (shortcut.getCategories().containsAll(Arrays.asList(item.mCategories))) {
-                    target = item;
+                    matchedShortcuts.add(new ShortcutHolder(shortcut,
+                            new ComponentName(context.getPackageName(), item.mTargetClass)));
                     break;
                 }
             }
-            if (target == null) {
-                continue;
-            }
+        }
+        return convertShortcutsToChooserTargets(shortcutSaver, matchedShortcuts);
+    }
 
-            IconCompat icon;
+    @VisibleForTesting
+    @NonNull
+    static List<ChooserTarget> convertShortcutsToChooserTargets(
+            @NonNull ShortcutInfoCompatSaverImpl shortcutSaver,
+            @NonNull List<ShortcutHolder> matchedShortcuts) {
+        if (matchedShortcuts.isEmpty()) {
+            return new ArrayList<>();
+        }
+        Collections.sort(matchedShortcuts);
+
+        ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
+        float currentScore = 1.0f;
+        int lastRank = matchedShortcuts.get(0).getShortcut().getRank();
+        for (ShortcutHolder holder : matchedShortcuts) {
+            final ShortcutInfoCompat shortcut = holder.getShortcut();
+            IconCompat shortcutIcon;
             try {
-                icon = shortcutSaver.getShortcutIcon(shortcut.getId());
+                shortcutIcon = shortcutSaver.getShortcutIcon(shortcut.getId());
             } catch (Exception e) {
                 Log.e(TAG, "Failed to retrieve shortcut icon: ", e);
-                continue;
+                shortcutIcon = null;
             }
+
             Bundle extras = new Bundle();
             extras.putString(ShortcutManagerCompat.EXTRA_SHORTCUT_ID, shortcut.getId());
+
+            if (lastRank != shortcut.getRank()) {
+                currentScore -= 0.01f;
+                lastRank = shortcut.getRank();
+            }
             chooserTargets.add(new ChooserTarget(
-                    // The name of this target.
                     shortcut.getShortLabel(),
-                    // The icon to represent this target.
-                    icon != null ? icon.toIcon() : null,
-                    // The ranking score for this target (0.0-1.0); the system will omit items with
-                    // low scores when there are too many Direct Share items.
-                    0.5f,
-                    // The name of the component to be launched if this target is chosen.
-                    new ComponentName(context.getPackageName(), target.mTargetClass),
-                    // The extra values here will be merged into the Intent when this target is
-                    // chosen.
+                    shortcutIcon == null ? null : shortcutIcon.toIcon(),
+                    currentScore,
+                    holder.getTargetClass(),
                     extras));
         }
+
         return chooserTargets;
     }
+
+    static class ShortcutHolder implements Comparable<ShortcutHolder> {
+        private final ShortcutInfoCompat mShortcut;
+        private final ComponentName mTargetClass;
+
+        ShortcutHolder(ShortcutInfoCompat shortcut, ComponentName targetClass) {
+            mShortcut = shortcut;
+            mTargetClass = targetClass;
+        }
+
+        ShortcutInfoCompat getShortcut() {
+            return mShortcut;
+        }
+
+        ComponentName getTargetClass() {
+            return mTargetClass;
+        }
+
+        @Override
+        public int compareTo(ShortcutHolder other) {
+            return this.getShortcut().getRank() - other.getShortcut().getRank();
+        }
+    }
 }
diff --git a/sharetarget/src/main/java/androidx/sharetarget/ShortcutsInfoSerialization.java b/sharetarget/src/main/java/androidx/sharetarget/ShortcutsInfoSerialization.java
index 97f9145..ef2ebc5 100644
--- a/sharetarget/src/main/java/androidx/sharetarget/ShortcutsInfoSerialization.java
+++ b/sharetarget/src/main/java/androidx/sharetarget/ShortcutsInfoSerialization.java
@@ -56,6 +56,7 @@
     private static final String ATTR_SHORT_LABEL = "short_label";
     private static final String ATTR_LONG_LABEL = "long_label";
     private static final String ATTR_DISABLED_MSG = "disabled_message";
+    private static final String ATTR_RANK = "rank";
 
     private static final String ATTR_ICON_RES_NAME = "icon_resource_name";
     private static final String ATTR_ICON_BMP_PATH = "icon_bitmap_path";
@@ -157,6 +158,7 @@
             return null;
         }
 
+        int rank = Integer.parseInt(getAttributeValue(parser, ATTR_RANK));
         CharSequence longLabel = getAttributeValue(parser, ATTR_LONG_LABEL);
         CharSequence disabledMessage = getAttributeValue(parser, ATTR_DISABLED_MSG);
         ComponentName activity = parseComponentName(parser);
@@ -188,7 +190,8 @@
         }
 
         ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, id)
-                .setShortLabel(label);
+                .setShortLabel(label)
+                .setRank(rank);
         if (!TextUtils.isEmpty(longLabel)) {
             builder.setLongLabel(longLabel);
         }
@@ -248,6 +251,7 @@
         ShortcutInfoCompat shortcut = container.mShortcutInfo;
         serializeAttribute(serializer, ATTR_ID, shortcut.getId());
         serializeAttribute(serializer, ATTR_SHORT_LABEL, shortcut.getShortLabel().toString());
+        serializeAttribute(serializer, ATTR_RANK, Integer.toString(shortcut.getRank()));
         if (!TextUtils.isEmpty(shortcut.getLongLabel())) {
             serializeAttribute(serializer, ATTR_LONG_LABEL, shortcut.getLongLabel().toString());
         }