• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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