Add multi-instance state to item infos

- Add legacy resource for supported multi-instance apps that
  matches the current SystemUI resource of the same name, and will
  be removed as apps migrate to the V manifest property to declare
  multi-instance support
- Load the multi-instance state from PackageManager when the db is
  first loaded or when packages are updated
- The multi-instance check is then used to determine if an app pair
  can be saved (ie. whether the action can be shown)

Bug: 323112914
Test: atest NexusLauncherTests

Change-Id: I565b4bee4ab5f7040910306b1fd60a4fc3bf9a1c
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 65a49bd..8b5ed7c 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -73,6 +73,7 @@
 import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PersistedItemArray;
 import com.android.quickstep.logging.SettingsChangeLogger;
 import com.android.quickstep.logging.StatsLogCompatManager;
@@ -150,7 +151,8 @@
         // TODO: Implement caching and preloading
 
         WorkspaceItemFactory factory =
-                new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, numColumns, state.containerId);
+                new WorkspaceItemFactory(mApp, ums, mPmHelper, pinnedShortcuts, numColumns,
+                        state.containerId);
         FixedContainerItems fci = new FixedContainerItems(state.containerId,
                 state.storage.read(mApp.getContext(), factory, ums.allUsers::get));
         if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
@@ -530,6 +532,7 @@
 
         private final LauncherAppState mAppState;
         private final UserManagerState mUMS;
+        private final PackageManagerHelper mPmHelper;
         private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
         private final int mMaxCount;
         private final int mContainer;
@@ -537,9 +540,11 @@
         private int mReadCount = 0;
 
         protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
-                Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount, int container) {
+                PackageManagerHelper pmHelper, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts,
+                int maxCount, int container) {
             mAppState = appState;
             mUMS = ums;
+            mPmHelper = pmHelper;
             mPinnedShortcuts = pinnedShortcuts;
             mMaxCount = maxCount;
             mContainer = container;
@@ -563,6 +568,7 @@
                             lai,
                             UserCache.INSTANCE.get(mAppState.getContext()).getUserInfo(user),
                             ApiWrapper.INSTANCE.get(mAppState.getContext()),
+                            mPmHelper,
                             mUMS.isUserQuiet(user));
                     info.container = mContainer;
                     mAppState.getIconCache().getTitleAndIcon(info, lai, false);
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index f052a9d..a53d91f 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -335,24 +335,15 @@
                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
             boolean shouldShowActionsButtonInstead =
                     isLargeTileFocusedTask && isInExpectedScrollPosition;
-            boolean hasUnpinnableApp = taskView.getTaskContainers().stream()
-                    .anyMatch(att -> att != null && att.getItemInfo() != null
-                            && ((att.getItemInfo().runtimeStatusFlags
-                                & ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0));
 
             // 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
-            // - at least one app in app pair is unpinnable
             // - the Overview Actions Button should be visible
-            // - the task is not a GroupedTaskView
-            if (!FeatureFlags.enableAppPairs()
-                    || !recentsView.supportsAppPairs()
-                    || !taskView.containsMultipleTasks()
-                    || hasUnpinnableApp
+            // - the task view is not a valid save-able split pair
+            if (!recentsView.supportsAppPairs()
                     || shouldShowActionsButtonInstead
-                    || !(taskView instanceof GroupedTaskView)) {
+                    || !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 770a452..e4e2eb2 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -22,6 +22,7 @@
 import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
 import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SUPPORTS_MULTI_INSTANCE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -32,34 +33,38 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.Cuj;
 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.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import com.android.launcher3.views.ActivityContext;
 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;
@@ -101,6 +106,55 @@
     }
 
     /**
+     * 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
+        boolean hasUnpinnableApp = taskView.getTaskContainers().stream()
+                .anyMatch(att -> att != null && att.getItemInfo() != null
+                        && ((att.getItemInfo().runtimeStatusFlags
+                            & ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0));
+        if (!FeatureFlags.enableAppPairs()
+                || !taskView.containsMultipleTasks()
+                || hasUnpinnableApp
+                || !(taskView instanceof GroupedTaskView)) {
+            return false;
+        }
+
+        GroupedTaskView gtv = (GroupedTaskView) taskView;
+        List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
+        ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask(
+                containers.get(0).getTask().key);
+        ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask(
+                containers.get(1).getTask().key);
+        AppInfo app1 = resolveAppInfoByComponent(taskKey1);
+        AppInfo app2 = resolveAppInfoByComponent(taskKey2);
+
+        if (app1 == null || app2 == null) {
+            // Disallow saving app pairs for apps that don't have a front-door in Launcher
+            return false;
+        }
+
+        if (PackageManagerHelper.isSameAppForMultiInstance(app1, app2)) {
+            if (!app1.supportsMultiInstance() || !app2.supportsMultiInstance()) {
+                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:
@@ -119,31 +173,23 @@
         List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
         WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo();
         WorkspaceItemInfo recentsInfo2 = containers.get(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 (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;
         }
-        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);
-        }
-
-        // WorkspaceItemProcessor won't process these new ItemInfos until the next launcher restart,
-        // so update some flags now.
-        updateWorkspaceItemFlags(app1);
-        updateWorkspaceItemFlags(app2);
 
         @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;
         }
 
@@ -229,67 +275,38 @@
     }
 
     /**
+     * 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 = ActivityContext.lookupContext(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 = ActivityContext.lookupContext(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;
-    }
-
-    /**
-     * Updates flags for newly created WorkspaceItemInfos.
-     */
-    private void updateWorkspaceItemFlags(WorkspaceItemInfo wii) {
-        PackageManager pm = mContext.getPackageManager();
-        ActivityInfo ai = null;
-        try {
-            ai = pm.getActivityInfo(wii.getTargetComponent(), 0);
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "PackageManager lookup failed.");
-        }
-
-        if (ai != null) {
-            wii.setNonResizeable(ai.resizeMode == ActivityInfo.RESIZE_MODE_UNRESIZEABLE);
-        }
-    }
-
-    /**
-     * Converts a WorkspaceItemInfo of itemType=ITEM_TYPE_TASK (from a Recents task) to a new
-     * WorkspaceItemInfo of itemType=ITEM_TYPE_APPLICATION.
-     */
-    private WorkspaceItemInfo convertRecentsItemToAppItem(WorkspaceItemInfo recentsItem) {
-        if (recentsItem.itemType != LauncherSettings.Favorites.ITEM_TYPE_TASK) {
-            Log.w(TAG, "Expected ItemInfo of type ITEM_TYPE_TASK, but received "
-                    + recentsItem.itemType);
-        }
-
-        WorkspaceItemInfo launchableItem = recentsItem.clone();
-        PackageManager p = mContext.getPackageManager();
-        Intent launchIntent = p.getLaunchIntentForPackage(recentsItem.getTargetPackage());
-        Log.w(TAG, "Initial intent from Recents: " + launchableItem.intent + "\n"
-                + "Intent from PackageManager: " + launchIntent);
-        if (launchIntent != null) {
-            // If lookup from PackageManager fails, just use the existing intent
-            launchableItem.intent = launchIntent;
-        }
-        launchableItem.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-        return launchableItem;
+        return appInfo.makeWorkspaceItem(mContext);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 8a917d6..ba60141 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -38,6 +38,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;
@@ -139,6 +140,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);
@@ -245,9 +247,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();
     }
 
@@ -264,7 +269,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 b5ce7f7..9c99dfc 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -4011,7 +4011,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);
@@ -4019,9 +4021,11 @@
         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);
 
-        boolean isCurrentDesktop = getCurrentPageTaskView() instanceof DesktopTaskView;
+        boolean isCurrentDesktop = taskView instanceof DesktopTaskView;
         mActionsView.updateHiddenFlags(HIDDEN_DESKTOP, isCurrentDesktop);
     }
 
diff --git a/res/values/config.xml b/res/values/config.xml
index 648a50c..a808a3f 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -223,4 +223,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>
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 869b995..a4ae1c8 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SafeCloseable;
@@ -181,7 +182,7 @@
         mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
                 iconCacheFileName, mIconProvider);
         mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext),
-                iconCacheFileName != null);
+                PackageManagerHelper.INSTANCE.get(context), iconCacheFileName != null);
         mOnTerminateCallback.add(mIconCache::close);
         mOnTerminateCallback.add(mModel::destroy);
     }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index be98589..e3da389 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -98,6 +98,8 @@
     @NonNull
     private final LauncherAppState mApp;
     @NonNull
+    private final PackageManagerHelper mPmHelper;
+    @NonNull
     private final ModelDbController mModelDbController;
     @NonNull
     private final Object mLock = new Object();
@@ -152,12 +154,13 @@
 
     LauncherModel(@NonNull final Context context, @NonNull final LauncherAppState app,
             @NonNull final IconCache iconCache, @NonNull final AppFilter appFilter,
-            final boolean isPrimaryInstance) {
+            @NonNull final PackageManagerHelper pmHelper, final boolean isPrimaryInstance) {
         mApp = app;
+        mPmHelper = pmHelper;
         mModelDbController = new ModelDbController(context);
         mBgAllAppsList = new AllAppsList(iconCache, appFilter);
-        mModelDelegate = ModelDelegate.newInstance(context, app, mBgAllAppsList, mBgDataModel,
-                isPrimaryInstance);
+        mModelDelegate = ModelDelegate.newInstance(context, app, mPmHelper, mBgAllAppsList,
+                mBgDataModel, isPrimaryInstance);
     }
 
     @NonNull
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index b9a62e2..98bba08 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -132,6 +132,10 @@
     @ChecksSdkIntAtLeast(api = VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "U")
     public static final boolean ATLEAST_U = Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE;
 
+    @ChecksSdkIntAtLeast(api = VERSION_CODES.VANILLA_ICE_CREAM, codename = "V")
+    public static final boolean ATLEAST_V = Build.VERSION.SDK_INT
+            >= VERSION_CODES.VANILLA_ICE_CREAM;
+
     /**
      * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
      */
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index 39c1243..bcd6ad2 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -301,6 +301,7 @@
             Context context, String packageName, UserHandle user) {
         final ApiWrapper apiWrapper = ApiWrapper.INSTANCE.get(context);
         final UserCache userCache = UserCache.getInstance(context);
+        final PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
         final List<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class)
                 .getActivityList(packageName, user);
         if (matches.size() > 0) {
@@ -330,7 +331,7 @@
                     applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title);
                     applicationInfo.intent = launchIntent;
                     AppInfo.updateRuntimeFlagsForActivityTarget(applicationInfo, info,
-                            userCache.getUserInfo(user), apiWrapper);
+                            userCache.getUserInfo(user), apiWrapper, pmHelper);
                     mDataChanged = true;
                 }
             }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 0875974..84130c7 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -57,6 +57,7 @@
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.UserIconInfo;
 
 import java.net.URISyntaxException;
@@ -73,6 +74,7 @@
 
     private final LauncherAppState mApp;
     private final Context mContext;
+    private final PackageManagerHelper mPmHelper;
     private final IconCache mIconCache;
     private final InvariantDeviceProfile mIDP;
     private final @Nullable LauncherRestoreEventLogger mRestoreEventLogger;
@@ -114,6 +116,7 @@
     public int restoreFlag;
 
     public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState,
+            PackageManagerHelper pmHelper,
             @Nullable LauncherRestoreEventLogger restoreEventLogger) {
         super(cursor);
 
@@ -121,6 +124,7 @@
         allUsers = userManagerState.allUsers;
         mContext = app.getContext();
         mIconCache = app.getIconCache();
+        mPmHelper = pmHelper;
         mIDP = app.getInvariantDeviceProfile();
         mRestoreEventLogger = restoreEventLogger;
 
@@ -368,7 +372,7 @@
 
         if (mActivityInfo != null) {
             AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo,
-                    ApiWrapper.INSTANCE.get(mContext));
+                    ApiWrapper.INSTANCE.get(mContext), mPmHelper);
         }
 
         // from the db
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 876bed4..0d40a24 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -435,7 +435,8 @@
             mShortcutKeyToPinnedShortcuts = new HashMap<>();
             final LoaderCursor c = new LoaderCursor(
                     dbController.query(TABLE_NAME, null, selection, null, null),
-                    mApp, mUserManagerState, mIsRestoreFromBackup ? restoreEventLogger : null);
+                    mApp, mUserManagerState, mPmHelper,
+                    mIsRestoreFromBackup ? restoreEventLogger : null);
             final Bundle extras = c.getExtras();
             mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME);
             try {
@@ -697,7 +698,7 @@
             for (int i = 0; i < apps.size(); i++) {
                 LauncherActivityInfo app = apps.get(i);
                 AppInfo appInfo = new AppInfo(app, mUserCache.getUserInfo(user),
-                        ApiWrapper.INSTANCE.get(mApp.getContext()), quietMode);
+                        ApiWrapper.INSTANCE.get(mApp.getContext()), mPmHelper, quietMode);
                 if (Flags.enableSupportForArchiving() && app.getApplicationInfo().isArchived) {
                     // For archived apps, include progress info in case there is a pending
                     // install session post restart of device.
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index 8360b14..2264d35 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.ResourceBasedOverride;
 
 import java.io.FileDescriptor;
@@ -41,15 +42,16 @@
      * Creates and initializes a new instance of the delegate
      */
     public static ModelDelegate newInstance(
-            Context context, LauncherAppState app, AllAppsList appsList, BgDataModel dataModel,
-            boolean isPrimaryInstance) {
+            Context context, LauncherAppState app, PackageManagerHelper pmHelper,
+            AllAppsList appsList, BgDataModel dataModel, boolean isPrimaryInstance) {
         ModelDelegate delegate = Overrides.getObject(
                 ModelDelegate.class, context, R.string.model_delegate_class);
-        delegate.init(app, appsList, dataModel, isPrimaryInstance);
+        delegate.init(app, pmHelper, appsList, dataModel, isPrimaryInstance);
         return delegate;
     }
 
     protected final Context mContext;
+    protected PackageManagerHelper mPmHelper;
     protected LauncherAppState mApp;
     protected AllAppsList mAppsList;
     protected BgDataModel mDataModel;
@@ -62,9 +64,10 @@
     /**
      * Initializes the object with the given params.
      */
-    private void init(LauncherAppState app, AllAppsList appsList,
+    private void init(LauncherAppState app, PackageManagerHelper pmHelper, AllAppsList appsList,
             BgDataModel dataModel, boolean isPrimaryInstance) {
         this.mApp = app;
+        this.mPmHelper = pmHelper;
         this.mAppsList = appsList;
         this.mDataModel = dataModel;
         this.mIsPrimaryInstance = isPrimaryInstance;
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 802faae..6275ed0 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -126,8 +126,8 @@
                     if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                         appsList.removePackage(packages[i], mUser);
                     }
-                    activitiesLists.put(
-                            packages[i], appsList.addPackage(context, packages[i], mUser));
+                    activitiesLists.put(packages[i],
+                            appsList.addPackage(context, packages[i], mUser));
                 }
                 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
@@ -138,8 +138,8 @@
                     for (int i = 0; i < N; i++) {
                         if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
                         iconCache.updateIconsForPkg(packages[i], mUser);
-                        activitiesLists.put(
-                                packages[i], appsList.updatePackage(context, packages[i], mUser));
+                        activitiesLists.put(packages[i],
+                                appsList.updatePackage(context, packages[i], mUser));
                     }
                 }
                 // Since package was just updated, the target must be available now.
@@ -269,6 +269,8 @@
                         if (isNewApkAvailable) {
                             List<LauncherActivityInfo> activities = activitiesLists.get(
                                     packageName);
+                            // TODO: See if we can migrate this to
+                            //  AppInfo#updateRuntimeFlagsForActivityTarget
                             si.setProgressLevel(
                                     activities == null || activities.isEmpty()
                                             ? 100
@@ -399,7 +401,8 @@
             return false;
         }
         // Try to find the best match activity.
-        Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser);
+        Intent intent = PackageManagerHelper.INSTANCE.get(context)
+                .getAppLaunchIntent(packageName, mUser);
         if (intent != null) {
             si.intent = intent;
             si.status = WorkspaceItemInfo.DEFAULT;
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index cea4380..90e47d6 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -336,7 +336,8 @@
                     info,
                     activityInfo,
                     userCache.getUserInfo(c.user),
-                    ApiWrapper.INSTANCE[app.context]
+                    ApiWrapper.INSTANCE[app.context],
+                    pmHelper
                 )
             }
             if (
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index 18aa6e7..a4281f8 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -90,12 +90,12 @@
      */
     public AppInfo(Context context, LauncherActivityInfo info, UserHandle user) {
         this(info, UserCache.INSTANCE.get(context).getUserInfo(user),
-                ApiWrapper.INSTANCE.get(context),
+                ApiWrapper.INSTANCE.get(context), PackageManagerHelper.INSTANCE.get(context),
                 context.getSystemService(UserManager.class).isQuietModeEnabled(user));
     }
 
     public AppInfo(LauncherActivityInfo info, UserIconInfo userIconInfo,
-            ApiWrapper apiWrapper, boolean quietModeEnabled) {
+            ApiWrapper apiWrapper, PackageManagerHelper pmHelper, boolean quietModeEnabled) {
         this.componentName = info.getComponentName();
         this.container = CONTAINER_ALL_APPS;
         this.user = userIconInfo.user;
@@ -105,7 +105,7 @@
             runtimeStatusFlags |= FLAG_DISABLED_QUIET_USER;
         }
         uid = info.getApplicationInfo().uid;
-        updateRuntimeFlagsForActivityTarget(this, info, userIconInfo, apiWrapper);
+        updateRuntimeFlagsForActivityTarget(this, info, userIconInfo, apiWrapper, pmHelper);
     }
 
     public AppInfo(AppInfo info) {
@@ -184,7 +184,7 @@
      */
     public static boolean updateRuntimeFlagsForActivityTarget(
             ItemInfoWithIcon info, LauncherActivityInfo lai, UserIconInfo userIconInfo,
-            ApiWrapper apiWrapper) {
+            ApiWrapper apiWrapper, PackageManagerHelper pmHelper) {
         final int oldProgressLevel = info.getProgressLevel();
         final int oldRuntimeStatusFlags = info.runtimeStatusFlags;
         ApplicationInfo appInfo = lai.getApplicationInfo();
@@ -216,6 +216,8 @@
                 PackageManagerHelper.getLoadingProgress(lai),
                 PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
         info.setNonResizeable(apiWrapper.isNonResizeableActivity(lai));
+        info.setSupportsMultiInstance(
+                pmHelper.supportsMultiInstance(lai.getComponentName()));
         return (oldProgressLevel != info.getProgressLevel())
                 || (oldRuntimeStatusFlags != info.runtimeStatusFlags);
     }
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index d4c25cb..6ac44ff 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -127,6 +127,11 @@
     public static final int FLAG_NOT_RESIZEABLE = 1 << 15;
 
     /**
+     * Flag indicating whether the package related to the item & user supports multiple instances.
+     */
+    public static final int FLAG_SUPPORTS_MULTI_INSTANCE = 1 << 16;
+
+    /**
      * Status associated with the system state of the underlying item. This is calculated every
      * time a new info is created and not persisted on the disk.
      */
@@ -252,6 +257,24 @@
     }
 
     /**
+     * Sets whether this app info supports multi-instance.
+     */
+    protected void setSupportsMultiInstance(boolean supportsMultiInstance) {
+        if (supportsMultiInstance) {
+            runtimeStatusFlags |= FLAG_SUPPORTS_MULTI_INSTANCE;
+        } else {
+            runtimeStatusFlags &= ~FLAG_SUPPORTS_MULTI_INSTANCE;
+        }
+    }
+
+    /**
+     * Returns whether this app info supports multi-instance.
+     */
+    public boolean supportsMultiInstance() {
+        return (runtimeStatusFlags & FLAG_SUPPORTS_MULTI_INSTANCE) != 0;
+    }
+
+    /**
      * Sets whether this app info is non-resizeable.
      */
     public void setNonResizeable(boolean nonResizeable) {
@@ -301,4 +324,11 @@
         drawable.setIsDisabled(isDisabled());
         return drawable;
     }
+
+    @Override
+    protected String dumpProperties() {
+        return super.dumpProperties()
+                + " supportsMultiInstance=" + supportsMultiInstance()
+                + " nonResizeable=" + isNonResizeable();
+    }
 }
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index b992a92..3ae643e 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.PackageManagerHelper;
 
 /**
  * A set of utility methods for Launcher DB used for DB updates and migration.
@@ -107,9 +108,11 @@
         Cursor c = db.query(
                 Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null);
         UserManagerState ums = new UserManagerState();
+        PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
         ums.init(UserCache.INSTANCE.get(context),
                 context.getSystemService(UserManager.class));
-        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, null);
+        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper,
+                null);
         IntSet deletedShortcuts = new IntSet();
 
         while (lc.moveToNext()) {
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 3684f56..f7c4df4 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.util;
 
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
+import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
 
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -73,10 +74,14 @@
     @NonNull
     private final LauncherApps mLauncherApps;
 
+    private final String[] mLegacyMultiInstanceSupportedApps;
+
     public PackageManagerHelper(@NonNull final Context context) {
         mContext = context;
         mPm = context.getPackageManager();
         mLauncherApps = Objects.requireNonNull(context.getSystemService(LauncherApps.class));
+        mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray(
+                R.array.config_appsSupportMultiInstancesSplit);
     }
 
     @Override
@@ -159,11 +164,23 @@
         }
     }
 
+    /**
+     * Returns the preferred launch activity intent for a given package.
+     */
     @Nullable
     public Intent getAppLaunchIntent(@Nullable final String pkg, @NonNull final UserHandle user) {
+        LauncherActivityInfo info = getAppLaunchInfo(pkg, user);
+        return info != null ? AppInfo.makeLaunchIntent(info) : null;
+    }
+
+    /**
+     * Returns the preferred launch activity for a given package.
+     */
+    @Nullable
+    public LauncherActivityInfo getAppLaunchInfo(@Nullable final String pkg,
+            @NonNull final UserHandle user) {
         List<LauncherActivityInfo> activities = mLauncherApps.getActivityList(pkg, user);
-        return activities.isEmpty() ? null :
-                AppInfo.makeLaunchIntent(activities.get(0));
+        return activities.isEmpty() ? null : activities.get(0);
     }
 
     /**
@@ -285,4 +302,47 @@
         return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || (
                 Flags.enableSupportForArchiving() && info.isArchived);
     }
+
+    /**
+     * Returns whether the given component or its application has the multi-instance property set.
+     */
+    public boolean supportsMultiInstance(@NonNull ComponentName component) {
+        // Check the legacy hardcoded allowlist first
+        for (String pkg : mLegacyMultiInstanceSupportedApps) {
+            if (pkg.equals(component.getPackageName())) {
+                return true;
+            }
+        }
+
+        // Check app multi-instance properties after V
+        if (!Utilities.ATLEAST_V) {
+            return false;
+        }
+
+        try {
+            // Check if the component has the multi-instance property
+            return mPm.getProperty(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, component)
+                    .getBoolean();
+        } catch (PackageManager.NameNotFoundException e1) {
+            try {
+                // Check if the application has the multi-instance property
+                return mPm.getProperty(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI,
+                                component.getPackageName())
+                    .getBoolean();
+            } catch (PackageManager.NameNotFoundException e2) {
+                // Fall through
+            }
+        }
+        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.
+     */
+    public static boolean isSameAppForMultiInstance(@NonNull ItemInfo app1,
+            @NonNull ItemInfo app2) {
+        return app1.getTargetPackage().equals(app2.getTargetPackage())
+                && app1.user.equals(app2.user);
+    }
 }
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index a9b75ea..8c5195e 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -412,5 +412,9 @@
             android:name="androidx.startup.InitializationProvider"
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
+
+        <property
+            android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
+            android:value="true"/>
     </application>
 </manifest>
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 56ac960..b4945d7 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -79,6 +79,7 @@
 
     private LauncherModelHelper mModelHelper;
     private LauncherAppState mApp;
+    private PackageManagerHelper mPmHelper;
 
     private MatrixCursor mCursor;
     private InvariantDeviceProfile mIDP;
@@ -92,6 +93,7 @@
         mContext = mModelHelper.sandboxContext;
         mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
         mApp = LauncherAppState.getInstance(mContext);
+        mPmHelper = PackageManagerHelper.INSTANCE.get(mContext);
 
         mCursor = new MatrixCursor(new String[] {
                 ICON, TITLE, _ID, CONTAINER, ITEM_TYPE,
@@ -101,7 +103,7 @@
         });
 
         UserManagerState ums = new UserManagerState();
-        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums, null);
+        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums, mPmHelper, null);
         ums.allUsers.put(0, Process.myUserHandle());
     }
 
diff --git a/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
index d1da5f4..b5e797e 100644
--- a/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
+++ b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
@@ -34,6 +34,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -63,6 +64,8 @@
         mContext = mock(Context.class);
         mLauncherApps = mock(LauncherApps.class);
         when(mContext.getSystemService(eq(LauncherApps.class))).thenReturn(mLauncherApps);
+        when(mContext.getResources()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getResources());
         mPackageManagerHelper = new PackageManagerHelper(mContext);
     }