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 0000000000..7d912a24c4 --- /dev/null +++ b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 782a705bfd..5e8b1f05e5 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -426,6 +426,9 @@ 16dp 16dp + + 8dp + 60dp 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 0000000000..c0c2a024b2 --- /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( + private val target: T, + private val itemInfo: ItemInfo?, + private val originalView: View?, + private val controllers: TaskbarControllers, +) : + SystemShortcut( + 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 { task: GroupTask? -> + task != null && task.task1.key.packageName == itemInfo?.getTargetPackage() + } + recentsModel.getTasks( + { tasks: List -> + // 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, + pendingTaskIds: MutableSet, + ) { + val taskList = arrayListOf>() + 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>) { + 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>, + 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 8ab2ffadcf..bdc7f92c25 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java @@ -47,6 +47,8 @@ public class TaskbarAutohideSuspendController implements 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 @@ public class TaskbarAutohideSuspendController implements 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 @@ public class TaskbarAutohideSuspendController implements "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 70d4bb10f2..2e0bae5200 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -201,8 +201,10 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba 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 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba * Returns a stream of Multi Instance menu options if an app supports it. */ Stream> getMultiInstanceMenuOptions() { - SystemShortcut.Factory factory = createNewWindowShortcutFactory(); - return factory != null ? Stream.of(factory) : Stream.empty(); - + SystemShortcut.Factory f1 = createNewWindowShortcutFactory(); + SystemShortcut.Factory f2 = createManageWindowsShortcutFactory(); + return f1 != null ? Stream.of(f1, f2) : Stream.empty(); } /** @@ -316,6 +318,23 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba }; } + /** + * 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 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. diff --git a/res/values/strings.xml b/res/values/strings.xml index 123e2b8dcf..c280307a43 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -47,6 +47,8 @@ New Window + + Manage Windows Save app pair