App Pairs (behind flag): Add new ItemInfo types and DB save functionality

This is the second of several patches implementing the App Pairs feature behind a flag.

This patch includes:
- AppPairsController, a new controller that will handle creation, launching, and deletion of app pairs
- ITEM_TYPE_APP_PAIR is a new type of FolderInfo (FolderInfo now can be either a folder or an app pair, and should probably be renamed to CollectionInfo or something more generic in future)
- Necessary plumbing for these new types
- Database code that handles saving a new app pair to the database with the correct schema

Flag: ENABLE_APP_PAIRS (set to false)
Bug: 274189428
Test: Not included in this CL, but will follow
Change-Id: Ie3aefd4eb9171f471789f54876de742849d3013b
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index fd7b343..8135238 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -128,19 +128,19 @@
      * A menu item, "Save app pair", that allows the user to preserve the current app combination as
      * a single persistent icon on the Home screen, allowing for quick split screen initialization.
      */
-    class SaveAppPairSystemShortcut extends SystemShortcut {
-
+    class SaveAppPairSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
         private final TaskView mTaskView;
 
-        public SaveAppPairSystemShortcut(BaseDraggingActivity target, TaskView taskView) {
-            super(R.drawable.ic_save_app_pair, R.string.save_app_pair, target,
+        public SaveAppPairSystemShortcut(BaseDraggingActivity activity, TaskView taskView) {
+            super(R.drawable.ic_save_app_pair, R.string.save_app_pair, activity,
                     taskView.getItemInfo(), taskView);
             mTaskView = taskView;
         }
 
         @Override
         public void onClick(View view) {
-            // TODO (b/274189428): Call "saveAppPair" function in new AppPairController class
+            ((RecentsView) mTarget.getOverviewPanel())
+                    .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
new file mode 100644
index 0000000..cbde257
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.Context;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Mini controller class that handles app pair interactions: saving, modifying, deleting, etc.
+ */
+public class AppPairsController {
+
+    private static final int POINT_THREE_RATIO = 0;
+    private static final int POINT_FIVE_RATIO = 1;
+    private static final int POINT_SEVEN_RATIO = 2;
+    /**
+     * Used to calculate {@link #complement(int)}
+     */
+    private static final int FULL_RATIO = 2;
+
+    private static final int LEFT_TOP = 0;
+    private static final int RIGHT_BOTTOM = 1 << 2;
+
+    // TODO (jeremysim b/274189428): Support saving different ratios in future.
+    public int DEFAULT_RATIO = POINT_FIVE_RATIO;
+
+    private final Context mContext;
+    private final SplitSelectStateController mSplitSelectStateController;
+    public AppPairsController(Context context,
+            SplitSelectStateController splitSelectStateController) {
+        mContext = context;
+        mSplitSelectStateController = splitSelectStateController;
+    }
+
+    /**
+     * Creates a new app pair ItemInfo and adds it to the workspace
+     */
+    public void saveAppPair(TaskView taskView) {
+        TaskView.TaskIdAttributeContainer[] attributes = taskView.getTaskIdAttributeContainers();
+        WorkspaceItemInfo app1 = attributes[0].getItemInfo().clone();
+        WorkspaceItemInfo app2 = attributes[1].getItemInfo().clone();
+        app1.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        app2.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        app1.rank = DEFAULT_RATIO + LEFT_TOP;
+        app2.rank = complement(DEFAULT_RATIO) + RIGHT_BOTTOM;
+        FolderInfo newAppPair = FolderInfo.createAppPair(app1, app2);
+        // TODO (jeremysim b/274189428): Generate default title here.
+        newAppPair.title = "App pair 1";
+
+        IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+        MODEL_EXECUTOR.execute(() -> {
+            newAppPair.contents.forEach(member -> {
+                member.title = "";
+                member.bitmap = iconCache.getDefaultIcon(newAppPair.user);
+                iconCache.getTitleAndIcon(member, member.usingLowResIcon());
+            });
+            MAIN_EXECUTOR.execute(() -> {
+                LauncherAccessibilityDelegate delegate =
+                        Launcher.getLauncher(mContext).getAccessibilityDelegate();
+                if (delegate != null) {
+                    MAIN_EXECUTOR.execute(() -> delegate.addToWorkspace(newAppPair, true));
+                }
+            });
+        });
+
+    }
+
+    /**
+     * Used to calculate the "opposite" side of the split ratio, so we can know how big the split
+     * apps are supposed to be. This math works because POINT_THREE_RATIO is internally represented
+     * by 0, POINT_FIVE_RATIO is represented by 1, and POINT_SEVEN_RATIO is represented by 2. There
+     * are no other supported ratios for now.
+     */
+    private int complement(int ratio1) {
+        int ratio2 = FULL_RATIO - ratio1;
+        return ratio2;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 11e1fbd..723d862 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -84,6 +84,7 @@
     private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
     private final SplitAnimationController mSplitAnimationController;
+    private final AppPairsController mAppPairsController;
     private StatsLogManager mStatsLogManager;
     private final SystemUiProxy mSystemUiProxy;
     private final StateManager mStateManager;
@@ -128,6 +129,7 @@
         mDepthController = depthController;
         mRecentTasksModel = recentsModel;
         mSplitAnimationController = new SplitAnimationController(this);
+        mAppPairsController = new AppPairsController(context, this);
     }
 
     /**
@@ -586,4 +588,8 @@
     public FloatingTaskView getFirstFloatingTaskView() {
         return mFirstFloatingTaskView;
     }
+
+    public AppPairsController getAppPairsController() {
+        return mAppPairsController;
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4f7380a..0e155a2 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -63,6 +63,8 @@
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch;
 
 import android.animation.Animator;
@@ -2475,6 +2477,12 @@
                             (FolderInfo) item);
                     break;
                 }
+                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
+                    FolderInfo info = (FolderInfo) item;
+                    // TODO (jeremysim b/274189428): Create app pair icon
+                    view = null;
+                    break;
+                }
                 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                 case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
                     view = inflateAppWidget((LauncherAppWidgetInfo) item);
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 1cd2a30..ad03221 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -112,6 +112,10 @@
          */
         public static final int ITEM_TYPE_DEEP_SHORTCUT = 6;
 
+        /**
+         * The favorite is an app pair for launching split screen
+         */
+        public static final int ITEM_TYPE_APP_PAIR = 10;
 
         // *** Below enum values are used for metrics purpose but not used in Favorites DB ***
 
@@ -256,6 +260,7 @@
                 case ITEM_TYPE_DEEP_SHORTCUT: return "DEEPSHORTCUT";
                 case ITEM_TYPE_TASK: return "TASK";
                 case ITEM_TYPE_QSB: return "QSB";
+                case ITEM_TYPE_APP_PAIR: return "APP_PAIR";
                 default: return String.valueOf(type);
             }
         }
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 27119ae..a7a25f4 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -408,6 +408,14 @@
                         LauncherSettings.Favorites.CONTAINER_DESKTOP,
                         screenId, coordinates[0], coordinates[1]);
                 mContext.bindItems(Collections.singletonList(info), true, accessibility);
+            } else if (item instanceof FolderInfo fi) {
+                mContext.getModelWriter().addItemToDatabase(fi,
+                        LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0],
+                        coordinates[1]);
+                fi.contents.forEach(member -> {
+                    mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1);
+                });
+                mContext.bindItems(Collections.singletonList(fi), true, accessibility);
             }
         }));
         return true;
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index b0f6e13..0e3b06c 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -190,14 +190,15 @@
         for (ItemInfo item : items) {
             switch (item.itemType) {
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
                     folders.remove(item.id);
                     if (FeatureFlags.IS_STUDIO_BUILD) {
                         for (ItemInfo info : itemsIdMap) {
                             if (info.container == item.id) {
                                 // We are deleting a folder which still contains items that
                                 // think they are contained by that folder.
-                                String msg = "deleting a folder (" + item + ") which still " +
-                                        "contains items (" + info + ")";
+                                String msg = "deleting a collection (" + item + ") which still "
+                                        + "contains items (" + info + ")";
                                 Log.e(TAG, msg);
                             }
                         }
@@ -238,6 +239,7 @@
         itemsIdMap.put(item.id, item);
         switch (item.itemType) {
             case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+            case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
                 folders.put(item.id, (FolderInfo) item);
                 workspaceItems.add(item);
                 break;
@@ -250,15 +252,14 @@
                 } else {
                     if (newItem) {
                         if (!folders.containsKey(item.container)) {
-                            // Adding an item to a folder that doesn't exist.
-                            String msg = "adding item: " + item + " to a folder that " +
-                                    " doesn't exist";
+                            // Adding an item to a nonexistent collection.
+                            String msg = "attempted to add item: " + item + " to a nonexistent app"
+                                    + " collection";
                             Log.e(TAG, msg);
                         }
                     } else {
                         findOrMakeFolder(item.container).add((WorkspaceItemInfo) item, false);
                     }
-
                 }
                 break;
             case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 524b769..e5a0eb1 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -114,6 +114,17 @@
     }
 
     /**
+     * Create an app pair, a type of app collection that launches multiple apps into split screen
+     */
+    public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+        FolderInfo newAppPair = new FolderInfo();
+        newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
+        newAppPair.contents.add(app1);
+        newAppPair.contents.add(app2);
+        return newAppPair;
+    }
+
+    /**
      * Add an app or shortcut
      *
      * @param item
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 41a603d..bfb80b3 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -90,6 +90,7 @@
      * {@link Favorites#ITEM_TYPE_SHORTCUT},
      * {@link Favorites#ITEM_TYPE_DEEP_SHORTCUT}
      * {@link Favorites#ITEM_TYPE_FOLDER},
+     * {@link Favorites#ITEM_TYPE_APP_PAIR},
      * {@link Favorites#ITEM_TYPE_APPWIDGET} or
      * {@link Favorites#ITEM_TYPE_CUSTOM_APPWIDGET}.
      */