Merge "Add multi-instance state to item infos" into 24D1-dev
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index daa6168..b80a3c9 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -322,14 +322,13 @@
                     isLargeTileFocusedTask && isInExpectedScrollPosition;
 
             // No "save app pair" menu item if:
-            // - app pairs feature is not enabled
             // - we are in 3p launcher
-            // - the task in question is a single task
             // - the Overview Actions Button should be visible
-            if (!FeatureFlags.enableAppPairs()
-                    || !recentsView.supportsAppPairs()
-                    || !taskView.containsMultipleTasks()
-                    || shouldShowActionsButtonInstead) {
+            // - the task view is not a valid save-able split pair
+            if (!recentsView.supportsAppPairs()
+                    || shouldShowActionsButtonInstead
+                    || !recentsView.getSplitSelectController().getAppPairsController()
+                            .canSaveAppPair(taskView)) {
                 return null;
             }
 
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 3ed3e40..fee3849 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -30,6 +30,7 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -38,6 +39,7 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -45,20 +47,25 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.apppairs.AppPairIcon;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.TaskView;
@@ -87,12 +94,16 @@
     private Context mContext;
     private final SplitSelectStateController mSplitSelectStateController;
     private final StatsLogManager mStatsLogManager;
+    private final String[] mLegacyMultiInstanceSupportedApps;
+
     public AppPairsController(Context context,
             SplitSelectStateController splitSelectStateController,
             StatsLogManager statsLogManager) {
         mContext = context;
         mSplitSelectStateController = splitSelectStateController;
         mStatsLogManager = statsLogManager;
+        mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray(
+                R.array.config_appsSupportMultiInstancesSplit);
     }
 
     void onDestroy() {
@@ -100,6 +111,72 @@
     }
 
     /**
+     * Returns whether the given component or its application supports multi-instance.
+     */
+    private boolean supportsMultiInstance(@NonNull ComponentName component) {
+        // Check the legacy hardcoded allowlist first
+        for (String pkg : mLegacyMultiInstanceSupportedApps) {
+            if (pkg.equals(component.getPackageName())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether two apps should be considered the same for multi-instance purposes, which
+     * requires additional checks to ensure they can be started as multiple instances.
+     */
+    private boolean isSameAppForMultiInstance(@NonNull ItemInfo app1,
+            @NonNull ItemInfo app2) {
+        return app1.getTargetPackage().equals(app2.getTargetPackage())
+                && app1.user.equals(app2.user);
+    }
+
+    /**
+     * Returns whether the specified GroupedTaskView can be saved as an app pair.
+     */
+    public boolean canSaveAppPair(TaskView taskView) {
+        if (mContext == null) {
+            // Can ignore as the activity is already destroyed
+            return false;
+        }
+
+        // Disallow saving app pairs if:
+        // - app pairs feature is not enabled
+        // - the task in question is a single task
+        // - at least one app in app pair is unpinnable
+        // - the task is not a GroupedTaskView
+        // - both tasks in the GroupedTaskView are from the same app and the app does not
+        //   support multi-instance
+        if (!FeatureFlags.enableAppPairs()
+                || !taskView.containsMultipleTasks()
+                || !(taskView instanceof GroupedTaskView)) {
+            return false;
+        }
+
+        GroupedTaskView gtv = (GroupedTaskView) taskView;
+        TaskView.TaskIdAttributeContainer[] attributes = gtv.getTaskIdAttributeContainers();
+        WorkspaceItemInfo info1 = attributes[0].getItemInfo();
+        WorkspaceItemInfo info2 = attributes[1].getItemInfo();
+        AppInfo app1 = resolveAppInfoByComponent(info1.getComponentKey());
+        AppInfo app2 = resolveAppInfoByComponent(info2.getComponentKey());
+
+        if (app1 == null || app2 == null) {
+            // Disallow saving app pairs for apps that don't have a front-door in Launcher
+            return false;
+        }
+
+        if (isSameAppForMultiInstance(app1, app2)) {
+            if (!supportsMultiInstance(app1.getTargetComponent()) 
+                    || !supportsMultiInstance(app2.getTargetComponent())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Creates a new app pair ItemInfo and adds it to the workspace.
      * <br>
      * We create WorkspaceItemInfos to save onto the app pair in the following way:
@@ -118,20 +195,16 @@
         TaskView.TaskIdAttributeContainer[] attributes = gtv.getTaskIdAttributeContainers();
         WorkspaceItemInfo recentsInfo1 = attributes[0].getItemInfo();
         WorkspaceItemInfo recentsInfo2 = attributes[1].getItemInfo();
-        WorkspaceItemInfo app1 = lookupLaunchableItem(recentsInfo1.getComponentKey());
-        WorkspaceItemInfo app2 = lookupLaunchableItem(recentsInfo2.getComponentKey());
+        WorkspaceItemInfo app1 = resolveAppPairWorkspaceInfo(recentsInfo1);
+        WorkspaceItemInfo app2 = resolveAppPairWorkspaceInfo(recentsInfo2);
 
-        // If app lookup fails, use the WorkspaceItemInfo that we have, but try to override default
-        // intent with one from PackageManager.
-        if (app1 == null) {
-            Log.w(TAG, "Creating an app pair, but app lookup for " + recentsInfo1.title
-                    + " failed. Falling back to the WorkspaceItemInfo from Recents.");
-            app1 = convertRecentsItemToAppItem(recentsInfo1);
-        }
-        if (app2 == null) {
-            Log.w(TAG, "Creating an app pair, but app lookup for " + recentsInfo2.title
-                    + " failed. Falling back to the WorkspaceItemInfo from Recents.");
-            app2 = convertRecentsItemToAppItem(recentsInfo2);
+        if (app1 == null || app2 == null) {
+            // This shouldn't happen if canSaveAppPair() is called above, but log an error and do
+            // not create the app pair if the workspace items can't be resolved
+            Log.w(TAG, "Failed to save app pair due to invalid apps ("
+                    + "app1=" + recentsInfo1.getComponentKey().componentName
+                    + " app2=" + recentsInfo2.getComponentKey().componentName + ")");
+            return;
         }
 
         // WorkspaceItemProcessor won't process these new ItemInfos until the next launcher restart,
@@ -141,8 +214,9 @@
 
         @PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
         if (!isPersistentSnapPosition(snapPosition)) {
-            // if we received an illegal snap position, log an error and do not create the app pair.
-            Log.wtf(TAG, "tried to save an app pair with illegal snapPosition " + snapPosition);
+            // If we received an illegal snap position, log an error and do not create the app pair
+            Log.wtf(TAG, "Tried to save an app pair with illegal snapPosition "
+                    + snapPosition);
             return;
         }
 
@@ -228,25 +302,36 @@
     }
 
     /**
+     * Returns an AppInfo associated with the app for the given ComponentKey, or null if no such
+     * package exists in the AllAppsStore.
+     */
+    @Nullable
+    private AppInfo resolveAppInfoByComponent(@NonNull ComponentKey key) {
+        AllAppsStore appsStore = Launcher.getLauncher(mContext).getAppsView().getAppsStore();
+
+        // First look up the app info in order of:
+        // - The exact activity for the recent task
+        // - The first(?) loaded activity from the package
+        AppInfo appInfo = appsStore.getApp(key);
+        if (appInfo == null) {
+            appInfo = appsStore.getApp(key, PACKAGE_KEY_COMPARATOR);
+        }
+        return appInfo;
+    }
+
+    /**
      * Creates a new launchable WorkspaceItemInfo of itemType=ITEM_TYPE_APPLICATION by looking the
      * ComponentKey up in the AllAppsStore. If no app is found, attempts a lookup by package
      * instead. If that lookup fails, returns null.
      */
     @Nullable
-    private WorkspaceItemInfo lookupLaunchableItem(@Nullable ComponentKey key) {
-        if (key == null) {
+    private WorkspaceItemInfo resolveAppPairWorkspaceInfo(
+            @NonNull WorkspaceItemInfo recentTaskInfo) {
+        // ComponentKey should never be null (see TaskView#getItemInfo)
+        AppInfo appInfo = resolveAppInfoByComponent(recentTaskInfo.getComponentKey());
+        if (appInfo == null) {
             return null;
         }
-
-        AllAppsStore appsStore = Launcher.getLauncher(mContext).getAppsView().getAppsStore();
-
-        // Lookup by ComponentKey
-        AppInfo appInfo = appsStore.getApp(key);
-        if (appInfo == null) {
-            // Lookup by package
-            appInfo = appsStore.getApp(key, PACKAGE_KEY_COMPARATOR);
-        }
-
         return appInfo != null ? appInfo.makeWorkspaceItem(mContext) : null;
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 4360cbc..208cea0 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.NavigationMode;
 import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
+import com.android.quickstep.util.AppPairsController;
 import com.android.quickstep.util.LayoutUtils;
 
 import java.lang.annotation.Retention;
@@ -133,6 +134,7 @@
     protected DeviceProfile mDp;
     private final Rect mTaskSize = new Rect();
     private boolean mIsGroupedTask = false;
+    private boolean mCanSaveAppPair = false;
 
     public OverviewActionsView(Context context) {
         this(context, null);
@@ -249,9 +251,12 @@
      * Updates a batch of flags to hide and show actions buttons when a grouped task (split screen)
      * is focused.
      * @param isGroupedTask True if the focused task is a grouped task.
+     * @param canSaveAppPair True if the focused task is a grouped task and can be saved as an app
+     *                      pair.
      */
-    public void updateForGroupedTask(boolean isGroupedTask) {
+    public void updateForGroupedTask(boolean isGroupedTask, boolean canSaveAppPair) {
         mIsGroupedTask = isGroupedTask;
+        mCanSaveAppPair = canSaveAppPair;
         updateActionButtonsVisibility();
     }
 
@@ -268,7 +273,7 @@
     private void updateActionButtonsVisibility() {
         assert mDp != null;
         boolean showSingleTaskActions = !mIsGroupedTask;
-        boolean showGroupActions = mIsGroupedTask && mDp.isTablet;
+        boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair;
         getActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showSingleTaskActions ? 1 : 0);
         getGroupActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showGroupActions ? 1 : 0);
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 12f83eb..f44455c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -4023,7 +4023,9 @@
      * * Device is large screen
      */
     private void updateCurrentTaskActionsVisibility() {
-        boolean isCurrentSplit = getCurrentPageTaskView() instanceof GroupedTaskView;
+        TaskView taskView = getCurrentPageTaskView();
+        boolean isCurrentSplit = taskView instanceof GroupedTaskView;
+        GroupedTaskView groupedTaskView = isCurrentSplit ? (GroupedTaskView) taskView : null;
         // Update flags to see if entire actions bar should be hidden.
         if (!FeatureFlags.enableAppPairs()) {
             mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
@@ -4031,7 +4033,9 @@
         mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive());
         // Update flags to see if actions bar should show buttons for a single task or a pair of
         // tasks.
-        mActionsView.updateForGroupedTask(isCurrentSplit);
+        boolean canSaveAppPair = isCurrentSplit && supportsAppPairs() &&
+                getSplitSelectController().getAppPairsController().canSaveAppPair(groupedTaskView);
+        mActionsView.updateForGroupedTask(isCurrentSplit, canSaveAppPair);
 
         if (isDesktopModeSupported()) {
             boolean isCurrentDesktop = getCurrentPageTaskView() instanceof DesktopTaskView;
diff --git a/res/values/config.xml b/res/values/config.xml
index c774a8c..4f65ea7 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -274,4 +274,11 @@
     <string-array name="skip_private_profile_shortcut_packages" translatable="false">
         <item>com.android.settings</item>
     </string-array>
+
+    <!-- Legacy list of components supporting multiple instances.
+         DO NOT ADD TO THIS LIST.  Apps should use the PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI
+         property to declare multi-instance support in V+. This resource should match the resource
+         of the same name in SystemUI. -->
+    <string-array name="config_appsSupportMultiInstancesSplit">
+    </string-array>
 </resources>