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
Merged-In: I565b4bee4ab5f7040910306b1fd60a4fc3bf9a1c
Change-Id: I9658b63672b692c42563c111c11be2433c98d535
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>