From ab8e999f81de0fd0593726aa42bc21faa0dc3d89 Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Wed, 8 May 2024 21:14:57 +0000 Subject: [PATCH] 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 --- .../quickstep/TaskShortcutFactory.java | 11 +- .../quickstep/util/AppPairsController.java | 139 ++++++++++++++---- .../quickstep/views/OverviewActionsView.java | 9 +- .../android/quickstep/views/RecentsView.java | 8 +- res/values/config.xml | 7 + 5 files changed, 137 insertions(+), 37 deletions(-) diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java index daa6168848..b80a3c92b8 100644 --- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java +++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java @@ -322,14 +322,13 @@ public interface TaskShortcutFactory { 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 3ed3e40ca7..fee3849209 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_POSIT 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.content.pm.PackageManager; 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.internal.jank.Cuj; 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,18 +94,88 @@ public class AppPairsController { 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() { mContext = null; } + /** + * 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. *
@@ -118,20 +195,16 @@ public class AppPairsController { 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 @@ public class AppPairsController { @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; } @@ -227,26 +301,37 @@ public class AppPairsController { ); } + /** + * 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 4360cbc6b5..208cea0d28 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.DisplayController; 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 @@ public class OverviewActionsView extends FrameLayo 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 @@ public class OverviewActionsView extends FrameLayo * 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 @@ public class OverviewActionsView extends FrameLayo 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 12f83eb8d7..f44455c2dd 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -4023,7 +4023,9 @@ public abstract class RecentsView com.android.settings + + + +