1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.taskbar 18 19 import android.content.Context 20 import android.graphics.Bitmap 21 import android.view.MotionEvent 22 import android.view.View 23 import com.android.launcher3.AbstractFloatingView 24 import com.android.launcher3.R 25 import com.android.launcher3.Utilities 26 import com.android.launcher3.model.data.ItemInfo 27 import com.android.launcher3.popup.SystemShortcut 28 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN 29 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext 30 import com.android.launcher3.util.TouchController 31 import com.android.launcher3.views.ActivityContext 32 import com.android.quickstep.RecentsModel 33 import com.android.quickstep.SystemUiProxy 34 import com.android.quickstep.util.DesktopTask 35 import com.android.systemui.shared.recents.model.Task 36 import com.android.systemui.shared.recents.model.ThumbnailData 37 import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason 38 import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer 39 40 /** 41 * A single menu item shortcut to execute displaying open instances of an app. Default interaction 42 * for [onClick] is to open the menu in a floating window. Touching one of the displayed tasks 43 * launches it. 44 */ 45 class ManageWindowsTaskbarShortcut<T>( 46 private val target: T, 47 private val itemInfo: ItemInfo?, 48 private val originalView: View, 49 private val controllers: TaskbarControllers, 50 ) : 51 SystemShortcut<T>( 52 R.drawable.desktop_mode_ic_taskbar_menu_manage_windows, 53 R.string.manage_windows_option_taskbar, 54 target, 55 itemInfo, 56 originalView, 57 ) where T : Context?, T : ActivityContext? { 58 private lateinit var taskbarShortcutAllWindowsView: TaskbarShortcutManageWindowsView 59 private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext] 60 61 override fun onClick(v: View?) { 62 val targetPackage = itemInfo?.getTargetPackage() 63 val targetUserId = itemInfo?.user?.identifier 64 val isTargetPackageTask: (Task) -> Boolean = { task -> 65 task.key?.packageName == targetPackage && task.key.userId == targetUserId 66 } 67 68 recentsModel.getTasks { tasks -> 69 val desktopTask = tasks.filterIsInstance<DesktopTask>().firstOrNull() 70 val packageDesktopTasks = 71 (desktopTask?.tasks ?: emptyList()).filter(isTargetPackageTask) 72 val nonDesktopPackageTasks = 73 tasks.flatMap { it.tasks }.filter { isTargetPackageTask(it) } 74 75 // Add tasks from the fetched tasks, deduplicating by task ID 76 val packageTasks = 77 (packageDesktopTasks + nonDesktopPackageTasks).distinctBy { it.key.id } 78 79 // Since fetching thumbnails is asynchronous, use `awaitedTaskIds` to gate until the 80 // tasks are ready to display 81 val awaitedTaskIds = packageTasks.map { it.key.id }.toMutableSet() 82 83 createAndShowTaskShortcutView(packageTasks, awaitedTaskIds) 84 } 85 } 86 87 /** 88 * Processes a list of tasks to generate thumbnails and create a taskbar shortcut view. 89 * 90 * Iterates through the tasks, retrieves thumbnails, and adds them to a list. When all 91 * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected 92 * thumbnails and positions it appropriately. 93 */ 94 private fun createAndShowTaskShortcutView(tasks: List<Task>, pendingTaskIds: MutableSet<Int>) { 95 val taskList = arrayListOf<Pair<Int, Bitmap?>>() 96 97 tasks.forEach { task -> 98 recentsModel.thumbnailCache.getThumbnailInBackground(task) { 99 thumbnailData: ThumbnailData -> 100 pendingTaskIds.remove(task.key.id) 101 // Add the current pair of task id and ThumbnailData to the list of all tasks 102 if (thumbnailData.thumbnail != null) { 103 taskList.add(task.key.id to thumbnailData.thumbnail) 104 } 105 // If the set is empty, all thumbnails have been fetched 106 if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) { 107 createAndPositionTaskbarShortcut(taskList) 108 } 109 } 110 } 111 } 112 113 /** 114 * Creates and positions the [TaskbarShortcutManageWindowsView] with the provided thumbnails. 115 */ 116 private fun createAndPositionTaskbarShortcut(taskList: ArrayList<Pair<Int, Bitmap?>>) { 117 val onIconClickListener = 118 ({ taskId: Int? -> 119 taskbarShortcutAllWindowsView.animateClose() 120 if (taskId != null) { 121 SystemUiProxy.INSTANCE.get(target) 122 .showDesktopApp( 123 taskId, 124 /* transition= */ null, 125 DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW, 126 ) 127 } 128 }) 129 130 val onOutsideClickListener = { taskbarShortcutAllWindowsView.animateClose() } 131 132 taskbarShortcutAllWindowsView = 133 TaskbarShortcutManageWindowsView( 134 originalView, 135 controllers.taskbarOverlayController.requestWindow(), 136 taskList, 137 onIconClickListener, 138 onOutsideClickListener, 139 controllers, 140 ) 141 142 // If the view is removed from elsewhere, reset the state to allow the taskbar to auto-stash 143 taskbarShortcutAllWindowsView.menuView.rootView.addOnAttachStateChangeListener( 144 object : View.OnAttachStateChangeListener { 145 override fun onViewAttachedToWindow(v: View) { 146 return 147 } 148 149 override fun onViewDetachedFromWindow(v: View) { 150 controllers.taskbarAutohideSuspendController.updateFlag( 151 FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, 152 false, 153 ) 154 controllers.taskbarPopupController.cleanUpMultiInstanceMenuReference() 155 } 156 } 157 ) 158 } 159 160 /** Closes the multi-instance menu if it has been initialized. */ 161 fun closeMultiInstanceMenu() { 162 if (::taskbarShortcutAllWindowsView.isInitialized) { 163 taskbarShortcutAllWindowsView.animateClose() 164 } 165 } 166 167 /** 168 * A view container for displaying the window of open instances of an app 169 * 170 * Handles showing the window snapshots, adding the carousel to the overlay, and closing it. 171 * Also acts as a touch controller to intercept touch events outside the carousel to close it. 172 */ 173 class TaskbarShortcutManageWindowsView( 174 private val originalView: View, 175 private val taskbarOverlayContext: TaskbarOverlayContext, 176 snapshotList: ArrayList<Pair<Int, Bitmap?>>, 177 onIconClickListener: (Int) -> Unit, 178 onOutsideClickListener: () -> Unit, 179 private val controllers: TaskbarControllers, 180 ) : 181 ManageWindowsViewContainer( 182 originalView.context, 183 originalView.context.getColor(R.color.materialColorSurfaceBright), 184 ), 185 TouchController { 186 private val taskbarActivityContext = controllers.taskbarActivityContext 187 188 init { 189 createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener) 190 taskbarOverlayContext.dragLayer.addTouchController(this) 191 animateOpen() 192 } 193 194 /** Adds the carousel menu to the taskbar overlay drag layer */ 195 override fun addToContainer(menuView: ManageWindowsView) { 196 positionCarouselMenu() 197 198 controllers.taskbarAutohideSuspendController.updateFlag( 199 FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, 200 true, 201 ) 202 AbstractFloatingView.closeAllOpenViewsExcept( 203 taskbarActivityContext, 204 AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY, 205 ) 206 menuView.rootView.minimumHeight = menuView.menuHeight 207 menuView.rootView.minimumWidth = menuView.menuWidth 208 209 taskbarOverlayContext.dragLayer?.addView(menuView.rootView) 210 menuView.rootView.requestFocus() 211 } 212 213 /** 214 * Positions the carousel menu relative to the taskbar and the calling app's icon. 215 * 216 * Calculates the Y position to place the carousel above the taskbar, and the X position to 217 * align with the calling app while ensuring it doesn't go beyond the screen edge. 218 */ 219 private fun positionCarouselMenu() { 220 val deviceProfile = taskbarActivityContext.deviceProfile 221 val margin = 222 context.resources.getDimension( 223 R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge 224 ) 225 226 // Calculate the Y position to place the carousel above the taskbar 227 menuView.rootView.y = 228 deviceProfile.availableHeightPx - 229 menuView.menuHeight - 230 controllers.taskbarStashController.touchableHeight - 231 margin 232 233 // Calculate the X position to align with the calling app, 234 // but avoid clashing with the screen edge 235 menuView.rootView.translationX = 236 if (Utilities.isRtl(context.resources)) { 237 -(deviceProfile.availableWidthPx - menuView.menuWidth) / 2f 238 } else { 239 val maxX = deviceProfile.availableWidthPx - menuView.menuWidth - margin 240 minOf(originalView.x, maxX) 241 } 242 } 243 244 /** Closes the carousel menu and removes it from the taskbar overlay drag layer */ 245 override fun removeFromContainer() { 246 controllers.taskbarAutohideSuspendController.updateFlag( 247 FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, 248 false, 249 ) 250 taskbarOverlayContext.dragLayer?.removeView(menuView.rootView) 251 taskbarOverlayContext.dragLayer.removeTouchController(this) 252 controllers.taskbarPopupController.cleanUpMultiInstanceMenuReference() 253 } 254 255 /** TouchController implementations for closing the carousel when touched outside */ 256 override fun onControllerTouchEvent(ev: MotionEvent?): Boolean { 257 return false 258 } 259 260 override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { 261 ev?.let { 262 if ( 263 it.action == MotionEvent.ACTION_DOWN && 264 !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, it) 265 ) { 266 animateClose() 267 } 268 } 269 return false 270 } 271 } 272 } 273