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>