From dbaf1028db04b9fdc1b371eea673b1e286f4d53f Mon Sep 17 00:00:00 2001 From: Saumya Prakash Date: Tue, 29 Oct 2024 00:10:15 +0000 Subject: [PATCH] Add Manage Windows option to Taskbar long press menu This change adds an option to the long press menu on Taskbar apps to view open instances of the calling apps. It will only show on apps that support multi instance (ex. Chrome). Bug: 315989246 Test: Manual Flag: com.android.launcher3.enable_multi_instance_menu_taskbar Change-Id: Ie1e001c4cec831c751bcbf448aaa68bb90fb24ca --- ...op_mode_ic_taskbar_menu_manage_windows.xml | 19 ++ quickstep/res/values/dimens.xml | 3 + .../taskbar/ManageWindowsTaskbarShortcut.kt | 239 ++++++++++++++++++ .../TaskbarAutohideSuspendController.java | 5 + .../taskbar/TaskbarPopupController.java | 27 +- res/values/strings.xml | 2 + 6 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml create mode 100644 quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt 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