Prototype for Sharesheet direct share row append mechanism

Use this cmd to enable the feature:
adb shell device_config put systemui append_direct_share_enabled true

Use this cmd to adjust timeout threshold (in millisecond):
adb shell device_config put systemui share_sheet_direct_share_timeout 15000

Bug: 151112858
Test: manually tested both prod flow and prototype flow on phones.

Change-Id: I328ecefc9dffad40ec412c033da54e0443f8889a
diff --git a/core/java/com/android/internal/app/AbstractResolverComparator.java b/core/java/com/android/internal/app/AbstractResolverComparator.java
index eb74612..e0bbc04 100644
--- a/core/java/com/android/internal/app/AbstractResolverComparator.java
+++ b/core/java/com/android/internal/app/AbstractResolverComparator.java
@@ -221,6 +221,12 @@
      */
     abstract float getScore(ComponentName name);
 
+    /**
+     * Returns the list of top K component names which have highest
+     * {@link #getScore(ComponentName)}
+     */
+    abstract List<ComponentName> getTopComponentNames(int topK);
+
     /** Handles result message sent to mHandler. */
     abstract void handleResultMessage(Message message);
 
diff --git a/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java b/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java
index 04ad7e9..ba8c7b3 100644
--- a/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java
+++ b/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java
@@ -36,7 +36,9 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
 
 /**
  * Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be
@@ -160,6 +162,15 @@
     }
 
     @Override
+    List<ComponentName> getTopComponentNames(int topK) {
+        return mTargetRanks.entrySet().stream()
+                .sorted(Entry.comparingByValue())
+                .limit(topK)
+                .map(Entry::getKey)
+                .collect(Collectors.toList());
+    }
+
+    @Override
     void updateModel(ComponentName componentName) {
         if (mResolverRankerService != null) {
             mResolverRankerService.updateModel(componentName);
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c487e96..5181a70 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -223,6 +223,11 @@
             SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
             DEFAULT_SALT_EXPIRATION_DAYS);
 
+    private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean(
+            DeviceConfig.NAMESPACE_SYSTEMUI,
+            SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED,
+            false);
+
     private Bundle mReplacementExtras;
     private IntentSender mChosenComponentSender;
     private IntentSender mRefinementIntentSender;
@@ -409,6 +414,11 @@
         private static final int WATCHDOG_TIMEOUT_MAX_MILLIS = 10000;
         private static final int WATCHDOG_TIMEOUT_MIN_MILLIS = 3000;
 
+        private static final int DEFAULT_DIRECT_SHARE_TIMEOUT_MILLIS = 1500;
+        private int mDirectShareTimeout = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.SHARE_SHEET_DIRECT_SHARE_TIMEOUT,
+                DEFAULT_DIRECT_SHARE_TIMEOUT_MILLIS);
+
         private boolean mMinTimeoutPassed = false;
 
         private void removeAllMessages() {
@@ -427,15 +437,14 @@
 
             if (DEBUG) {
                 Log.d(TAG, "queryTargets setting watchdog timer for "
-                        + WATCHDOG_TIMEOUT_MIN_MILLIS + "-"
+                        + mDirectShareTimeout + "-"
                         + WATCHDOG_TIMEOUT_MAX_MILLIS + "ms");
             }
 
             sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT,
                     WATCHDOG_TIMEOUT_MIN_MILLIS);
             sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT,
-                    WATCHDOG_TIMEOUT_MAX_MILLIS);
-
+                    mAppendDirectShareEnabled ? mDirectShareTimeout : WATCHDOG_TIMEOUT_MAX_MILLIS);
         }
 
         private void maybeStopServiceRequestTimer() {
@@ -463,6 +472,7 @@
                     final ServiceResultInfo sri = (ServiceResultInfo) msg.obj;
                     if (!mServiceConnections.contains(sri.connection)) {
                         Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection
+                                + sri.originalTarget.getResolveInfo().activityInfo.packageName
                                 + " returned after being removed from active connections."
                                 + " Have you considered returning results faster?");
                         break;
@@ -474,7 +484,7 @@
                         if (adapterForUserHandle != null) {
                             adapterForUserHandle.addServiceResults(sri.originalTarget,
                                     sri.resultTargets, TARGET_TYPE_CHOOSER_TARGET,
-                                    /* directShareShortcutInfoCache */ null);
+                                    /* directShareShortcutInfoCache */ null, mServiceConnections);
                         }
                     }
                     unbindService(sri.connection);
@@ -489,6 +499,7 @@
                     break;
 
                 case CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT:
+                    mMinTimeoutPassed = true;
                     unbindRemainingServices();
                     maybeStopServiceRequestTimer();
                     break;
@@ -513,7 +524,7 @@
                         if (adapterForUserHandle != null) {
                             adapterForUserHandle.addServiceResults(
                                     resultInfo.originalTarget, resultInfo.resultTargets, msg.arg1,
-                                    mDirectShareShortcutInfoCache);
+                                    mDirectShareShortcutInfoCache, mServiceConnections);
                         }
                     }
                     break;
@@ -1481,7 +1492,7 @@
                     /* origTarget */ null,
                     Lists.newArrayList(mCallerChooserTargets),
                     TARGET_TYPE_DEFAULT,
-                    /* directShareShortcutInfoCache */ null);
+                    /* directShareShortcutInfoCache */ null, mServiceConnections);
         }
     }
 
@@ -3567,6 +3578,10 @@
                     ? mOriginalTarget.getResolveInfo().activityInfo.toString()
                     : "<connection destroyed>") + "}";
         }
+
+        public ComponentName getComponentName() {
+            return mOriginalTarget.getResolveInfo().activityInfo.getComponentName();
+        }
     }
 
     static class ServiceResultInfo {
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 74ae291..0ea855a 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -32,8 +32,10 @@
 import android.os.AsyncTask;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.DeviceConfig;
 import android.service.chooser.ChooserTarget;
 import android.util.Log;
+import android.util.Pair;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -44,17 +46,26 @@
 import com.android.internal.app.chooser.MultiDisplayResolveInfo;
 import com.android.internal.app.chooser.SelectableTargetInfo;
 import com.android.internal.app.chooser.TargetInfo;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 public class ChooserListAdapter extends ResolverListAdapter {
     private static final String TAG = "ChooserListAdapter";
     private static final boolean DEBUG = false;
 
+    private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean(
+            DeviceConfig.NAMESPACE_SYSTEMUI,
+            SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED,
+            false);
+
     private boolean mEnableStackedApps = true;
 
     public static final int NO_POSITION = -1;
@@ -84,6 +95,11 @@
     // Reserve spots for incoming direct share targets by adding placeholders
     private ChooserTargetInfo
             mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
+    private int mValidServiceTargetsNum = 0;
+    private final Map<ComponentName, Pair<List<ChooserTargetInfo>, Integer>>
+            mParkingDirectShareTargets = new HashMap<>();
+    private Set<ComponentName> mPendingChooserTargetService = new HashSet<>();
+    private Set<ComponentName> mShortcutComponents = new HashSet<>();
     private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
     private final List<TargetInfo> mCallerTargets = new ArrayList<>();
 
@@ -189,6 +205,9 @@
 
     void refreshListView() {
         if (mListViewDataChanged) {
+            if (mAppendDirectShareEnabled) {
+                appendServiceTargetsWithQuota();
+            }
             super.notifyDataSetChanged();
         }
         mListViewDataChanged = false;
@@ -198,6 +217,10 @@
     private void createPlaceHolders() {
         mNumShortcutResults = 0;
         mServiceTargets.clear();
+        mValidServiceTargetsNum = 0;
+        mParkingDirectShareTargets.clear();
+        mPendingChooserTargetService.clear();
+        mShortcutComponents.clear();
         for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
             mServiceTargets.add(mPlaceHolderTargetInfo);
         }
@@ -393,12 +416,19 @@
      */
     public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
             @ChooserActivity.ShareTargetType int targetType,
-            Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
+            Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+            List<ChooserActivity.ChooserTargetServiceConnection>
+                    pendingChooserTargetServiceConnections) {
         if (DEBUG) {
-            Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
+            Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
+                    + targets.size()
                     + " targets");
         }
-
+        if (mAppendDirectShareEnabled) {
+            parkTargetIntoMemory(origTarget, targets, targetType, directShareToShortcutInfos,
+                    pendingChooserTargetServiceConnections);
+            return;
+        }
         if (targets.size() == 0) {
             return;
         }
@@ -449,6 +479,126 @@
         }
     }
 
+    /**
+     * Park {@code targets} into memory for the moment to surface them later when view is refreshed.
+     * Components pending on ChooserTargetService query are also recorded.
+     */
+    private void parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
+            @ChooserActivity.ShareTargetType int targetType,
+            Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+            List<ChooserActivity.ChooserTargetServiceConnection>
+                    pendingChooserTargetServiceConnections) {
+        ComponentName origComponentName = origTarget.getResolvedComponentName();
+        mPendingChooserTargetService = pendingChooserTargetServiceConnections.stream()
+                .map(ChooserActivity.ChooserTargetServiceConnection::getComponentName)
+                .filter(componentName -> !componentName.equals(origComponentName))
+                .collect(Collectors.toSet());
+        // Park targets in memory
+        if (!targets.isEmpty() && !mParkingDirectShareTargets.containsKey(origComponentName)) {
+            final boolean isShortcutResult =
+                    (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+                            || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+            Context contextAsUser = mContext.createContextAsUser(getUserHandle(),
+                    0 /* flags */);
+            List<ChooserTargetInfo> parkingTargetInfos = targets.stream()
+                    .map(target ->
+                            new SelectableTargetInfo(
+                                    contextAsUser, origTarget, target, target.getScore(),
+                                    mSelectableTargetInfoComunicator,
+                                    (isShortcutResult ? directShareToShortcutInfos.get(target)
+                                            : null))
+                    )
+                    .collect(Collectors.toList());
+            mParkingDirectShareTargets.put(origComponentName,
+                    new Pair<>(parkingTargetInfos, 0));
+            if (isShortcutResult) {
+                mShortcutComponents.add(origComponentName);
+            }
+        }
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Append targets of top ranked share app into direct share row with quota limit. Remove
+     * appended ones from memory.
+     */
+    private void appendServiceTargetsWithQuota() {
+        int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
+        List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
+        int appRank = 0;
+        for (ComponentName component : topComponentNames) {
+            if (!mPendingChooserTargetService.contains(component)
+                    && !mParkingDirectShareTargets.containsKey(component)) {
+                continue;
+            }
+            appRank++;
+            Pair<List<ChooserTargetInfo>, Integer> parkingTargetsItem =
+                    mParkingDirectShareTargets.get(component);
+            if (parkingTargetsItem != null && parkingTargetsItem.second == 0) {
+                List<ChooserTargetInfo> parkingTargets = parkingTargetsItem.first;
+                int initTargetsQuota = appRank <= maxRankedTargets / 2 ? 2 : 1;
+                int insertedNum = 0;
+                while (insertedNum < initTargetsQuota && !parkingTargets.isEmpty()) {
+                    if (!checkDuplicateTarget(parkingTargets.get(0))) {
+                        mServiceTargets.add(mValidServiceTargetsNum, parkingTargets.get(0));
+                        mValidServiceTargetsNum++;
+                        insertedNum++;
+                    }
+                    parkingTargets.remove(0);
+                }
+                mParkingDirectShareTargets.put(component, new Pair<>(parkingTargets, insertedNum));
+                if (mShortcutComponents.contains(component)) {
+                    mNumShortcutResults += insertedNum;
+                }
+            }
+        }
+    }
+
+    /**
+     * Append all remaining targets (parking in memory) into direct share row as per their ranking.
+     */
+    private void fillAllServiceTargets() {
+        int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
+        List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
+        // Append all remaining targets of top recommended components into direct share row.
+        for (ComponentName component : topComponentNames) {
+            if (!mParkingDirectShareTargets.containsKey(component)) {
+                continue;
+            }
+            mParkingDirectShareTargets.get(component).first.stream()
+                    .filter(target -> !checkDuplicateTarget(target))
+                    .forEach(target -> {
+                        if (mShortcutComponents.contains(component)) {
+                            mNumShortcutResults++;
+                        }
+                        mServiceTargets.add(target);
+                    });
+            mParkingDirectShareTargets.remove(component);
+        }
+        // Append all remaining shortcuts targets into direct share row.
+        List<ChooserTargetInfo> remainingTargets = new ArrayList<>();
+        mParkingDirectShareTargets.entrySet().stream()
+                .filter(entry -> mShortcutComponents.contains(entry.getKey()))
+                .map(entry -> entry.getValue())
+                .map(pair -> pair.first)
+                .forEach(remainingTargets::addAll);
+        remainingTargets.sort(
+                (t1, t2) -> -Float.compare(t1.getModifiedScore(), t2.getModifiedScore()));
+        mServiceTargets.addAll(remainingTargets);
+        mNumShortcutResults += remainingTargets.size();
+        mParkingDirectShareTargets.clear();
+    }
+
+    private boolean checkDuplicateTarget(ChooserTargetInfo chooserTargetInfo) {
+        // Check for duplicates and abort if found
+        for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
+            if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     int getNumShortcutResults() {
         return mNumShortcutResults;
     }
@@ -487,7 +637,9 @@
      */
     public void completeServiceTargetLoading() {
         mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
-
+        if (mAppendDirectShareEnabled) {
+            fillAllServiceTargets();
+        }
         if (mServiceTargets.isEmpty()) {
             mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
         }
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 54453d0..b06750b 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -153,6 +153,14 @@
         return mResolverListController.getScore(target);
     }
 
+    /**
+     * Returns the list of top K component names which have highest
+     * {@link #getScore(DisplayResolveInfo)}
+     */
+    public List<ComponentName> getTopComponentNames(int topK) {
+        return mResolverListController.getTopComponentNames(topK);
+    }
+
     public void updateModel(ComponentName componentName) {
         mResolverListController.updateModel(componentName);
     }
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
index 022aded..51dce55 100644
--- a/core/java/com/android/internal/app/ResolverListController.java
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -367,6 +367,14 @@
         return mResolverComparator.getScore(target.getResolvedComponentName());
     }
 
+    /**
+     * Returns the list of top K component names which have highest
+     * {@link #getScore(DisplayResolveInfo)}
+     */
+    public List<ComponentName> getTopComponentNames(int topK) {
+        return mResolverComparator.getTopComponentNames(topK);
+    }
+
     public void updateModel(ComponentName componentName) {
         mResolverComparator.updateModel(componentName);
     }
diff --git a/core/java/com/android/internal/app/ResolverRankerServiceResolverComparator.java b/core/java/com/android/internal/app/ResolverRankerServiceResolverComparator.java
index 01e0622..2869450 100644
--- a/core/java/com/android/internal/app/ResolverRankerServiceResolverComparator.java
+++ b/core/java/com/android/internal/app/ResolverRankerServiceResolverComparator.java
@@ -48,6 +48,7 @@
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * Ranks and compares packages based on usage stats and uses the {@link ResolverRankerService}.
@@ -252,6 +253,15 @@
         return 0;
     }
 
+    @Override
+    List<ComponentName> getTopComponentNames(int topK) {
+        return mTargetsDict.entrySet().stream()
+                .sorted((o1, o2) -> -Float.compare(getScore(o1.getKey()), getScore(o2.getKey())))
+                .limit(topK)
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toList());
+    }
+
     // update ranking model when the connection to it is valid.
     @Override
     public void updateModel(ComponentName componentName) {
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 086b9d8..837cc46 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -378,6 +378,17 @@
             "nav_bar_handle_show_over_lockscreen";
 
     /**
+     * (int) Timeout threshold, in millisecond, that Sharesheet waits for direct share targets.
+     */
+    public static final String SHARE_SHEET_DIRECT_SHARE_TIMEOUT =
+            "share_sheet_direct_share_timeout";
+
+    /**
+     * (boolean) Whether append direct share on Sharesheet is enabled.
+     */
+    public static final String APPEND_DIRECT_SHARE_ENABLED = "append_direct_share_enabled";
+
+    /**
      * (boolean) Whether to enable user-drag resizing for PIP.
      */
     public static final String PIP_USER_RESIZE = "pip_user_resize";