Merge "Add Manage Windows option to Taskbar long press menu" into main
diff --git a/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml
new file mode 100644
index 0000000..7d912a2
--- /dev/null
+++ b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+    <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/>
+</vector>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 70efe14..53f37ba 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -431,6 +431,9 @@
     <dimen name="taskbar_pinning_popup_menu_vertical_margin">16dp</dimen>
     <dimen name="taskbar_pinning_popup_menu_min_padding_from_screen_edge">16dp</dimen>
 
+    <!-- Taskbar Multi Instance Menu -->
+    <dimen name="taskbar_multi_instance_menu_min_padding_from_screen_edge">8dp</dimen>
+
     <!--- Floating Ime Inset height-->
     <dimen name="floating_ime_inset_height">60dp</dimen>
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
new file mode 100644
index 0000000..c0c2a02
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2024 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.launcher3.taskbar
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.popup.SystemShortcut
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext
+import com.android.launcher3.util.Themes
+import com.android.launcher3.util.TouchController
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer
+import java.util.Collections
+import java.util.function.Predicate
+
+/**
+ * A single menu item shortcut to execute displaying open instances of an app. Default interaction
+ * for [onClick] is to open the menu in a floating window. Touching one of the displayed tasks
+ * launches it.
+ */
+class ManageWindowsTaskbarShortcut<T>(
+    private val target: T,
+    private val itemInfo: ItemInfo?,
+    private val originalView: View?,
+    private val controllers: TaskbarControllers,
+) :
+    SystemShortcut<T>(
+        R.drawable.desktop_mode_ic_taskbar_menu_manage_windows,
+        R.string.manage_windows_option_taskbar,
+        target,
+        itemInfo,
+        originalView,
+    ) where T : Context?, T : ActivityContext? {
+    private lateinit var taskbarShortcutAllWindowsView: TaskbarShortcutManageWindowsView
+    private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext]
+
+    override fun onClick(v: View?) {
+        val filter =
+            Predicate<GroupTask> { task: GroupTask? ->
+                task != null && task.task1.key.packageName == itemInfo?.getTargetPackage()
+            }
+        recentsModel.getTasks(
+            { tasks: List<GroupTask> ->
+                // Since fetching thumbnails is asynchronous, use this set to gate until the tasks
+                // are ready to display
+                val pendingTaskIds =
+                    Collections.synchronizedSet(tasks.map { it.task1.key.id }.toMutableSet())
+                createAndShowTaskShortcutView(tasks, pendingTaskIds)
+            },
+            filter,
+        )
+    }
+
+    /**
+     * Processes a list of tasks to generate thumbnails and create a taskbar shortcut view.
+     *
+     * Iterates through the tasks, retrieves thumbnails, and adds them to a list. When all
+     * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected
+     * thumbnails and positions it appropriately.
+     */
+    private fun createAndShowTaskShortcutView(
+        tasks: List<GroupTask?>,
+        pendingTaskIds: MutableSet<Int>,
+    ) {
+        val taskList = arrayListOf<Pair<Int, Bitmap?>>()
+        tasks.forEach { groupTask ->
+            groupTask?.task1?.let { task ->
+                recentsModel.thumbnailCache.getThumbnailInBackground(task) {
+                    thumbnailData: ThumbnailData ->
+                    pendingTaskIds.remove(task.key.id)
+                    // Add the current pair of task id and ThumbnailData to the list of all tasks
+                    if (thumbnailData.thumbnail != null) {
+                        taskList.add(task.key.id to thumbnailData.thumbnail)
+                    }
+
+                    // If the set is empty, all thumbnails have been fetched
+                    if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) {
+                        createAndPositionTaskbarShortcut(taskList)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates and positions the [TaskbarShortcutManageWindowsView] with the provided thumbnails.
+     */
+    private fun createAndPositionTaskbarShortcut(taskList: ArrayList<Pair<Int, Bitmap?>>) {
+        val onIconClickListener =
+            ({ taskId: Int? ->
+                taskbarShortcutAllWindowsView.removeFromContainer()
+                if (taskId != null) {
+                    SystemUiProxy.INSTANCE.get(target).showDesktopApp(taskId, null)
+                }
+            })
+
+        val onOutsideClickListener = { taskbarShortcutAllWindowsView.removeFromContainer() }
+
+        taskbarShortcutAllWindowsView =
+            TaskbarShortcutManageWindowsView(
+                originalView!!,
+                controllers.taskbarOverlayController.requestWindow(),
+                taskList,
+                onIconClickListener,
+                onOutsideClickListener,
+                controllers,
+            )
+    }
+
+    /**
+     * A view container for displaying the window of open instances of an app
+     *
+     * Handles showing the window snapshots, adding the carousel to the overlay, and closing it.
+     * Also acts as a touch controller to intercept touch events outside the carousel to close it.
+     */
+    class TaskbarShortcutManageWindowsView(
+        private val originalView: View,
+        private val taskbarOverlayContext: TaskbarOverlayContext,
+        snapshotList: ArrayList<Pair<Int, Bitmap?>>,
+        onIconClickListener: (Int) -> Unit,
+        onOutsideClickListener: () -> Unit,
+        private val controllers: TaskbarControllers,
+    ) :
+        ManageWindowsViewContainer(
+            originalView.context,
+            Themes.getAttrColor(originalView.context, R.attr.materialColorSurfaceBright),
+        ),
+        TouchController {
+        private val taskbarActivityContext = controllers.taskbarActivityContext
+
+        init {
+            createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener)
+            taskbarOverlayContext.dragLayer.addTouchController(this)
+        }
+
+        /** Adds the carousel menu to the taskbar overlay drag layer */
+        override fun addToContainer(menuView: ManageWindowsView) {
+            taskbarOverlayContext.dragLayer.post { positionCarouselMenu() }
+
+            controllers.taskbarAutohideSuspendController.updateFlag(
+                FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+                true,
+            )
+            AbstractFloatingView.closeAllOpenViewsExcept(
+                taskbarActivityContext,
+                AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY,
+            )
+            menuView.rootView.minimumHeight = menuView.menuHeight
+            menuView.rootView.minimumWidth = menuView.menuWidth
+
+            taskbarOverlayContext.dragLayer?.addView(menuView.rootView)
+            menuView.rootView.requestFocus()
+        }
+
+        /**
+         * Positions the carousel menu relative to the taskbar and the calling app's icon.
+         *
+         * Calculates the Y position to place the carousel above the taskbar, and the X position to
+         * align with the calling app while ensuring it doesn't go beyond the screen edge.
+         */
+        private fun positionCarouselMenu() {
+            val margin =
+                context.resources.getDimension(
+                    R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge
+                )
+
+            // Calculate the Y position to place the carousel above the taskbar
+            val availableHeight = taskbarOverlayContext.dragLayer.height
+            menuView.rootView.y =
+                availableHeight -
+                    menuView.menuHeight -
+                    controllers.taskbarStashController.touchableHeight -
+                    margin
+
+            // Calculate the X position to align with the calling app,
+            // but avoid clashing with the screen edge
+            val availableWidth = taskbarOverlayContext.dragLayer.width
+            if (Utilities.isRtl(context.resources)) {
+                menuView.rootView.translationX = -(availableWidth - menuView.menuWidth) / 2f
+            } else {
+                val maxX = availableWidth - menuView.menuWidth - margin
+                menuView.rootView.translationX = minOf(originalView.x, maxX)
+            }
+        }
+
+        /** Closes the carousel menu and removes it from the taskbar overlay drag layer */
+        override fun removeFromContainer() {
+            controllers.taskbarAutohideSuspendController.updateFlag(
+                FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+                false,
+            )
+            controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
+            taskbarOverlayContext.dragLayer?.removeView(menuView.rootView)
+            taskbarOverlayContext.dragLayer.removeTouchController(this)
+        }
+
+        /** TouchController implementations for closing the carousel when touched outside */
+        override fun onControllerTouchEvent(ev: MotionEvent?): Boolean {
+            return false
+        }
+
+        override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
+            ev?.let {
+                if (
+                    ev.action == MotionEvent.ACTION_DOWN &&
+                        !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, ev)
+                ) {
+                    removeFromContainer()
+                }
+            }
+            return false
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 8ab2ffa..bdc7f92 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -47,6 +47,8 @@
     public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5;
     // User has hovered the taskbar.
     public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
+    // User has multi instance window open.
+    public static final int FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN = 1 << 7;
 
     @IntDef(flag = true, value = {
             FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -56,6 +58,7 @@
             FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER,
             FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
             FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+            FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutohideSuspendFlag {}
@@ -133,6 +136,8 @@
                 "FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER");
         appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
                 "FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR");
+        appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+                "FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN");
         return str.toString();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 70d4bb1..2e0bae5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -201,8 +201,10 @@
         if (com.android.wm.shell.Flags.enableBubbleAnything()) {
             shortcuts.add(BUBBLE);
         }
+
         if (Flags.enableMultiInstanceMenuTaskbar()
-                && DesktopModeStatus.canEnterDesktopMode(mContext)) {
+                && DesktopModeStatus.canEnterDesktopMode(mContext)
+                && !mControllers.taskbarStashController.isInOverview()) {
             shortcuts.addAll(getMultiInstanceMenuOptions().toList());
         }
         return shortcuts.stream();
@@ -295,9 +297,9 @@
      * Returns a stream of Multi Instance menu options if an app supports it.
      */
     Stream<SystemShortcut.Factory<BaseTaskbarContext>> getMultiInstanceMenuOptions() {
-        SystemShortcut.Factory<BaseTaskbarContext> factory = createNewWindowShortcutFactory();
-        return factory != null ? Stream.of(factory) : Stream.empty();
-
+        SystemShortcut.Factory<BaseTaskbarContext> f1 = createNewWindowShortcutFactory();
+        SystemShortcut.Factory<BaseTaskbarContext> f2 = createManageWindowsShortcutFactory();
+        return f1 != null ? Stream.of(f1, f2) : Stream.empty();
     }
 
     /**
@@ -317,6 +319,23 @@
     }
 
     /**
+     * Creates a factory function representing a "Manage Windows" menu item only if the calling app
+     * supports multi-instance. This menu item shows the open instances of the calling app.
+     * @return A factory function to be used in populating the long-press menu.
+     */
+    public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() {
+        return (context, itemInfo, originalView) -> {
+            ComponentKey key = itemInfo.getComponentKey();
+            AppInfo app = getApp(key);
+            if (app != null && app.supportsMultiInstance()) {
+                return new ManageWindowsTaskbarShortcut<>(context, itemInfo, originalView,
+                        mControllers);
+            }
+            return null;
+        };
+    }
+
+    /**
      * A single menu item ("Split left," "Split right," or "Split top") that executes a split
      * from the taskbar, as if the user performed a drag and drop split.
      * Includes an onClick method that initiates the actual split.
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 123e2b8..c280307 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -47,6 +47,8 @@
 
     <!-- Title for an option to open a new window for a given app   -->
     <string name="new_window_option_taskbar">New Window</string>
+    <!-- Title for an option to manage open windows for a given app   -->
+    <string name="manage_windows_option_taskbar">Manage Windows</string>
 
     <!-- App pairs -->
     <string name="save_app_pair">Save app pair</string>