• 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.quickstep.views
18 
19 import android.graphics.PointF
20 import android.graphics.Rect
21 import android.util.FloatProperty
22 import android.view.KeyEvent
23 import android.view.View
24 import android.view.View.LAYOUT_DIRECTION_LTR
25 import android.view.View.LAYOUT_DIRECTION_RTL
26 import androidx.core.view.children
27 import com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU
28 import com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType
29 import com.android.launcher3.Flags.enableGridOnlyOverview
30 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
31 import com.android.launcher3.Flags.enableOverviewIconMenu
32 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
33 import com.android.launcher3.Utilities.getPivotsForScalingRectToRect
34 import com.android.launcher3.statehandlers.DesktopVisibilityController
35 import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID
36 import com.android.launcher3.util.IntArray
37 import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
38 import com.android.quickstep.util.DesktopTask
39 import com.android.quickstep.util.GroupTask
40 import com.android.quickstep.util.isExternalDisplay
41 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
42 import com.android.systemui.shared.recents.model.Task
43 import com.android.systemui.shared.recents.model.ThumbnailData
44 import com.android.wm.shell.shared.GroupedTaskInfo
45 import java.util.function.BiConsumer
46 import kotlin.math.min
47 import kotlin.reflect.KMutableProperty1
48 
49 /**
50  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
51  * RecentsView to facilitate the implementation of unit tests.
52  */
53 class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
54     val taskViews = TaskViewsIterable(recentsView)
55 
56     /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */
57     fun screenshotTasks(taskView: TaskView): Map<Int, ThumbnailData> {
58         val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap()
59         return taskView.taskContainers.associate {
60             it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id)
61         }
62     }
63 
64     /**
65      * Sorts task groups to move desktop tasks to the end of the list.
66      *
67      * @param tasks List of group tasks to be sorted.
68      * @return Sorted list of GroupTasks to be used in the RecentsView.
69      */
70     fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
71         var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
72         if (areMultiDesksFlagsEnabled()) {
73             // Desk IDs of newer desks are larger than those of older desks, hence we can use them
74             // to sort desks from old to new.
75             desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId }
76         }
77         return otherTasks + desktopTasks
78     }
79 
80     fun sortExternalDisplayTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
81         val (externalDisplayTasks, otherTasks) =
82             tasks.partition { it.tasks.firstOrNull().isExternalDisplay }
83         return otherTasks + externalDisplayTasks
84     }
85 
86     class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable<TaskView> {
87         /** Iterates TaskViews when its index inside the RecentsView is needed. */
88         fun forEachWithIndexInParent(consumer: BiConsumer<Int, TaskView>) {
89             recentsView.children.forEachIndexed { index, child ->
90                 (child as? TaskView)?.let { consumer.accept(index, it) }
91             }
92         }
93 
94         override fun iterator(): Iterator<TaskView> =
95             recentsView.children.mapNotNull { it as? TaskView }.iterator()
96     }
97 
98     /** Counts [TaskView]s that are [DesktopTaskView] instances. */
99     private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
100 
101     /** Counts [TaskView]s that are not [DesktopTaskView] instances. */
102     fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
103 
104     /** Returns a list of all large TaskView Ids from [TaskView]s */
105     fun getLargeTaskViewIds(): List<Int> = taskViews.filter { it.isLargeTile }.map { it.taskViewId }
106 
107     /** Returns a list of all large TaskViews [TaskView]s */
108     fun getLargeTaskViews(): List<TaskView> = taskViews.filter { it.isLargeTile }
109 
110     /** Returns all the TaskViews in the top row, without the focused task */
111     fun getTopRowTaskViews(): List<TaskView> =
112         taskViews.filter { recentsView.mTopRowIdSet.contains(it.taskViewId) }
113 
114     /** Returns all the task Ids in the top row, without the focused task */
115     fun getTopRowIdArray(): IntArray = getTopRowTaskViews().map { it.taskViewId }.toIntArray()
116 
117     /** Returns all the TaskViews in the bottom row, without the focused task */
118     fun getBottomRowTaskViews(): List<TaskView> =
119         taskViews.filter { !recentsView.mTopRowIdSet.contains(it.taskViewId) && !it.isLargeTile }
120 
121     /** Returns all the task Ids in the bottom row, without the focused task */
122     fun getBottomRowIdArray(): IntArray = getBottomRowTaskViews().map { it.taskViewId }.toIntArray()
123 
124     private fun List<Int>.toIntArray() = IntArray(size).apply { this@toIntArray.forEach(::add) }
125 
126     /** Counts [TaskView]s that are large tiles. */
127     fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile }
128 
129     /** Counts [TaskView]s that are grid tasks. */
130     fun getGridTaskCount(): Int = taskViews.count { it.isGridTask }
131 
132     /** Returns the first TaskView that should be displayed as a large tile. */
133     fun getFirstLargeTaskView(): TaskView? =
134         taskViews.firstOrNull {
135             it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
136         }
137 
138     /**
139      * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist.
140      */
141     fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? {
142         if (deskId == INACTIVE_DESK_ID) {
143             return null
144         }
145         return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId }
146             as? DesktopTaskView
147     }
148 
149     /** Returns the active desk ID of the display that contains the [recentsView] instance. */
150     fun getActiveDeskIdOnThisDisplay(): Int =
151         DesktopVisibilityController.INSTANCE.get(recentsView.context)
152             .getActiveDeskId(recentsView.mContainer.display.displayId)
153 
154     /** Returns the expected focus task. */
155     fun getFirstNonDesktopTaskView(): TaskView? =
156         if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
157         else taskViews.firstOrNull()
158 
159     /**
160      * Returns the [TaskView] that should be the current page during task binding, in the following
161      * priorities:
162      * 1. Running task
163      * 2. Focused task
164      * 3. First non-desktop task
165      * 4. Last desktop task
166      * 5. null otherwise
167      */
168     fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? =
169         runningTaskView
170             ?: focusedTaskView
171             ?: taskViews.firstOrNull {
172                 it !is DesktopTaskView &&
173                     !(enableSeparateExternalDisplayTasks() && it.isExternalDisplay)
174             }
175             ?: taskViews.lastOrNull()
176 
177     private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile
178 
179     fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int {
180         if (areMultiDesksFlagsEnabled() && runningTaskView is DesktopTaskView) {
181             // Use the [deskId] to keep desks in the order of their creation, as a newer desk
182             // always has a larger [deskId] than the older desks.
183             val desktopTaskView =
184                 taskViews.firstOrNull {
185                     it is DesktopTaskView &&
186                         it.deskId != INACTIVE_DESK_ID &&
187                         it.deskId <= runningTaskView.deskId
188                 }
189             if (desktopTaskView != null) return recentsView.indexOfChild(desktopTaskView)
190         }
191         val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView())
192         return if (getDeviceProfile().isTablet) {
193             var index = firstTaskViewIndex
194             if (enableLargeDesktopWindowingTile() && runningTaskView !is DesktopTaskView) {
195                 // For fullsreen tasks, skip over Desktop tasks in its section
196                 index +=
197                     if (enableSeparateExternalDisplayTasks()) {
198                         if (runningTaskView.isExternalDisplay) {
199                             taskViews.count { it is DesktopTaskView && it.isExternalDisplay }
200                         } else {
201                             taskViews.count { it is DesktopTaskView && !it.isExternalDisplay }
202                         }
203                     } else {
204                         getDesktopTaskViewCount()
205                     }
206             }
207             if (enableSeparateExternalDisplayTasks() && !runningTaskView.isExternalDisplay) {
208                 // For main display section, skip over external display tasks
209                 index += taskViews.count { it.isExternalDisplay }
210             }
211             index
212         } else {
213             val currentIndex: Int = recentsView.indexOfChild(runningTaskView)
214             return if (currentIndex != -1) {
215                 currentIndex // Keep the position if running task already in layout.
216             } else {
217                 // New running task are added to the front to begin with.
218                 firstTaskViewIndex
219             }
220         }
221     }
222 
223     /** Returns the first TaskView if it exists, or null otherwise. */
224     fun getFirstTaskView(): TaskView? = taskViews.firstOrNull()
225 
226     /** Returns the last TaskView if it exists, or null otherwise. */
227     fun getLastTaskView(): TaskView? = taskViews.lastOrNull()
228 
229     /** Returns the first TaskView that is not large */
230     fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile }
231 
232     /** Returns the last TaskView that should be displayed as a large tile. */
233     fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile }
234 
235     /**
236      * Gets the list of accessibility children. Currently all the children of RecentsViews are
237      * added, and in the reverse order to the list.
238      */
239     fun getAccessibilityChildren(): List<View> = recentsView.children.toList().reversed()
240 
241     @JvmOverloads
242     /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */
243     fun getFirstTaskViewInCarousel(
244         nonRunningTaskCarouselHidden: Boolean,
245         runningTaskView: TaskView? = recentsView.runningTaskView,
246     ): TaskView? =
247         taskViews.firstOrNull {
248             it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
249         }
250 
251     /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */
252     fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? =
253         taskViews.lastOrNull {
254             it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden)
255         }
256 
257     /** Returns if any small tasks are fully visible */
258     fun isAnySmallTaskFullyVisible(): Boolean =
259         taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) }
260 
261     /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */
262     fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) {
263         taskViews.forEach { taskView ->
264             taskView.attachAlpha =
265                 if (taskView == recentsView.runningTaskView) {
266                     RUNNING_TASK_ATTACH_ALPHA.get(recentsView)
267                 } else {
268                     if (
269                         taskView.isVisibleInCarousel(
270                             recentsView.runningTaskView,
271                             nonRunningTaskCarouselHidden,
272                         )
273                     )
274                         1f
275                     else 0f
276                 }
277         }
278     }
279 
280     fun TaskView.isVisibleInCarousel(
281         runningTaskView: TaskView?,
282         nonRunningTaskCarouselHidden: Boolean,
283     ): Boolean =
284         if (!nonRunningTaskCarouselHidden) true
285         else getCarouselType() == runningTaskView.getCarouselType()
286 
287     /** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */
288     private fun TaskView?.getCarouselType(): TaskViewCarousel =
289         if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN
290 
291     private enum class TaskViewCarousel {
292         FULL_SCREEN,
293         DESKTOP,
294     }
295 
296     /** Returns true if there are at least one TaskView has been added to the RecentsView. */
297     fun hasTaskViews() = taskViews.any()
298 
299     fun getTaskContainerById(taskId: Int) =
300         taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) }
301 
302     private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) {
303         outRowRect.setEmpty()
304         firstView?.let {
305             it.getHitRect(TEMP_RECT)
306             outRowRect.union(TEMP_RECT)
307         }
308         lastView?.let {
309             it.getHitRect(TEMP_RECT)
310             outRowRect.union(TEMP_RECT)
311         }
312     }
313 
314     private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) {
315         if (rowTaskViewIds.isEmpty) {
316             outRowRect.setEmpty()
317             return
318         }
319         getRowRect(
320             recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)),
321             recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)),
322             outRowRect,
323         )
324     }
325 
326     fun updateTaskViewDeadZoneRect(
327         outTaskViewRowRect: Rect,
328         outTopRowRect: Rect,
329         outBottomRowRect: Rect,
330     ) {
331         if (!getDeviceProfile().isTablet) {
332             getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect)
333             return
334         }
335         getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect)
336         getRowRect(getTopRowIdArray(), outTopRowRect)
337         getRowRect(getBottomRowIdArray(), outBottomRowRect)
338 
339         // Expand large tile Rect to include space between top/bottom row.
340         val nonEmptyRowRect =
341             when {
342                 !outTopRowRect.isEmpty -> outTopRowRect
343                 !outBottomRowRect.isEmpty -> outBottomRowRect
344                 else -> return
345             }
346         if (recentsView.isRtl) {
347             if (outTaskViewRowRect.left > nonEmptyRowRect.right) {
348                 outTaskViewRowRect.left = nonEmptyRowRect.right
349             }
350         } else {
351             if (outTaskViewRowRect.right < nonEmptyRowRect.left) {
352                 outTaskViewRowRect.right = nonEmptyRowRect.left
353             }
354         }
355 
356         // Expand the shorter row Rect to include the space between the 2 rows.
357         if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return
358         if (outTopRowRect.width() <= outBottomRowRect.width()) {
359             if (outTopRowRect.bottom < outBottomRowRect.top) {
360                 outTopRowRect.bottom = outBottomRowRect.top
361             }
362         } else {
363             if (outBottomRowRect.top > outTopRowRect.bottom) {
364                 outBottomRowRect.top = outTopRowRect.bottom
365             }
366         }
367     }
368 
369     private fun getTaskMenu(): TaskMenuView? =
370         getTopOpenViewWithType(recentsView.mContainer, TYPE_TASK_MENU) as? TaskMenuView
371 
372     fun shouldInterceptKeyEvent(event: KeyEvent): Boolean {
373         if (enableOverviewIconMenu()) {
374             return getTaskMenu()?.isOpen == true || event.keyCode == KeyEvent.KEYCODE_TAB
375         }
376         return false
377     }
378 
379     fun updateChildTaskOrientations() {
380         with(recentsView) {
381             taskViews.forEach { it.setOrientationState(mOrientationState) }
382             if (enableOverviewIconMenu()) {
383                 children.forEach {
384                     it.layoutDirection = if (isRtl) LAYOUT_DIRECTION_LTR else LAYOUT_DIRECTION_RTL
385                 }
386             }
387 
388             // Return when it's not fake landscape
389             if (mOrientationState.isRecentsActivityRotationAllowed) return@with
390 
391             // Rotation is supported on phone (details at b/254198019#comment4)
392             getTaskMenu()?.onRotationChanged()
393         }
394     }
395 
396     fun updateCentralTask() {
397         val isTablet: Boolean = getDeviceProfile().isTablet
398         val actionsViewCanRelateToTaskView = !(isTablet && enableGridOnlyOverview())
399         val focusedTaskView = recentsView.focusedTaskView
400         val currentPageTaskView = recentsView.currentPageTaskView
401 
402         fun isInExpectedScrollPosition(taskView: TaskView?) =
403             taskView?.let { recentsView.isTaskInExpectedScrollPosition(it) } ?: false
404 
405         val centralTaskIds: Set<Int> =
406             when {
407                 !actionsViewCanRelateToTaskView -> emptySet()
408                 isTablet && isInExpectedScrollPosition(focusedTaskView) ->
409                     focusedTaskView!!.taskIdSet
410                 isInExpectedScrollPosition(currentPageTaskView) -> currentPageTaskView!!.taskIdSet
411                 else -> emptySet()
412             }
413 
414         recentsView.mRecentsViewModel.updateCentralTaskIds(centralTaskIds)
415     }
416 
417     var deskExplodeProgress: Float = 0f
418         set(value) {
419             field = value
420             taskViews.filterIsInstance<DesktopTaskView>().forEach { it.explodeProgress = field }
421         }
422 
423     var selectedTaskView: TaskView? = null
424         set(newValue) {
425             val oldValue = field
426             field = newValue
427             if (oldValue != newValue) {
428                 onSelectedTaskViewUpdated(oldValue, newValue)
429             }
430         }
431 
432     private fun onSelectedTaskViewUpdated(
433         oldSelectedTaskView: TaskView?,
434         newSelectedTaskView: TaskView?,
435     ) {
436         if (!enableGridOnlyOverview()) return
437         with(recentsView) {
438             oldSelectedTaskView?.modalScale = 1f
439             oldSelectedTaskView?.modalPivot = null
440 
441             if (newSelectedTaskView == null) return
442 
443             val modalTaskBounds = mTempRect
444             getModalTaskSize(modalTaskBounds)
445             val selectedTaskBounds = getTaskBounds(newSelectedTaskView)
446 
447             // Map bounds to selectedTaskView's coordinate system.
448             modalTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top)
449             selectedTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top)
450 
451             val modalScale =
452                 min(
453                     (modalTaskBounds.height().toFloat() / selectedTaskBounds.height()),
454                     (modalTaskBounds.width().toFloat() / selectedTaskBounds.width()),
455                 )
456             val modalPivot = PointF()
457             getPivotsForScalingRectToRect(modalTaskBounds, selectedTaskBounds, modalPivot)
458 
459             newSelectedTaskView.modalScale = modalScale
460             newSelectedTaskView.modalPivot = modalPivot
461         }
462     }
463 
464     /**
465      * Creates a [DesktopTaskView] for the currently active desk on this display, which contains the
466      * tasks with the given [groupedTaskInfo].
467      */
468     fun createDesktopTaskViewForActiveDesk(groupedTaskInfo: GroupedTaskInfo): DesktopTaskView {
469         val desktopTaskView =
470             recentsView.getTaskViewFromPool(TaskViewType.DESKTOP) as DesktopTaskView
471         val tasks: List<Task> = groupedTaskInfo.taskInfoList.map { taskInfo -> Task.from(taskInfo) }
472         desktopTaskView.bind(
473             DesktopTask(groupedTaskInfo.deskId, groupedTaskInfo.deskDisplayId, tasks),
474             recentsView.mOrientationState,
475             recentsView.mTaskOverlayFactory,
476         )
477         return desktopTaskView
478     }
479 
480     companion object {
481         class RecentsViewFloatProperty(
482             private val utilsProperty: KMutableProperty1<RecentsViewUtils, Float>
483         ) : FloatProperty<RecentsView<*, *>>(utilsProperty.name) {
484             override fun get(recentsView: RecentsView<*, *>): Float =
485                 utilsProperty.get(recentsView.mUtils)
486 
487             override fun setValue(recentsView: RecentsView<*, *>, value: Float) {
488                 utilsProperty.set(recentsView.mUtils, value)
489             }
490         }
491 
492         @JvmField
493         val DESK_EXPLODE_PROGRESS = RecentsViewFloatProperty(RecentsViewUtils::deskExplodeProgress)
494 
495         val TEMP_RECT = Rect()
496     }
497 }
498