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.wm.shell.desktopmode
18 
19 import android.app.ActivityManager.RunningTaskInfo
20 import android.os.IBinder
21 import android.view.SurfaceControl
22 import android.view.WindowManager.TRANSIT_TO_BACK
23 import android.window.TransitionInfo
24 import android.window.WindowContainerTransaction
25 import androidx.annotation.VisibleForTesting
26 import com.android.wm.shell.ShellTaskOrganizer
27 import com.android.wm.shell.protolog.ShellProtoLogGroup
28 import com.android.wm.shell.shared.DesktopModeStatus
29 import com.android.wm.shell.transition.Transitions
30 import com.android.wm.shell.transition.Transitions.TransitionObserver
31 import com.android.wm.shell.util.KtProtoLog
32 
33 /**
34  * Limits the number of tasks shown in Desktop Mode.
35  *
36  * This class should only be used if
37  * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true.
38  */
39 class DesktopTasksLimiter (
40         transitions: Transitions,
41         private val taskRepository: DesktopModeTaskRepository,
42         private val shellTaskOrganizer: ShellTaskOrganizer,
43 ) {
44     private val minimizeTransitionObserver = MinimizeTransitionObserver()
45 
46     init {
47         transitions.registerObserver(minimizeTransitionObserver)
48     }
49 
50     private data class TaskDetails (val displayId: Int, val taskId: Int)
51 
52     // TODO(b/333018485): replace this observer when implementing the minimize-animation
53     private inner class MinimizeTransitionObserver : TransitionObserver {
54         private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
55 
56         fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) {
57             mPendingTransitionTokensAndTasks[transition] = taskDetails
58         }
59 
60         override fun onTransitionReady(
61                 transition: IBinder,
62                 info: TransitionInfo,
63                 startTransaction: SurfaceControl.Transaction,
64                 finishTransaction: SurfaceControl.Transaction
65         ) {
66             val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return
67 
68             if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return
69 
70             if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) {
71                 KtProtoLog.v(
72                         ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
73                         "DesktopTasksLimiter: task %d is not reordered to back nor invis",
74                         taskToMinimize.taskId)
75                 return
76             }
77             this@DesktopTasksLimiter.markTaskMinimized(
78                     taskToMinimize.displayId, taskToMinimize.taskId)
79         }
80 
81         /**
82          * Returns whether the given Task is being reordered to the back in the given transition, or
83          * is already invisible.
84          *
85          * <p> This check can be used to double-check that a task was indeed minimized before
86          * marking it as such.
87          */
88         private fun isTaskReorderedToBackOrInvisible(
89                 info: TransitionInfo,
90                 taskDetails: TaskDetails
91         ): Boolean {
92             val taskChange = info.changes.find { change ->
93                 change.taskInfo?.taskId == taskDetails.taskId }
94             if (taskChange == null) {
95                 return !taskRepository.isVisibleTask(taskDetails.taskId)
96             }
97             return taskChange.mode == TRANSIT_TO_BACK
98         }
99 
100         override fun onTransitionStarting(transition: IBinder) {}
101 
102         override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
103             mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer ->
104                 mPendingTransitionTokensAndTasks[playing] = taskToTransfer
105             }
106         }
107 
108         override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
109             KtProtoLog.v(
110                     ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
111                     "DesktopTasksLimiter: transition %s finished", transition)
112             mPendingTransitionTokensAndTasks.remove(transition)
113         }
114     }
115 
116     /**
117      * Mark a task as minimized, this should only be done after the corresponding transition has
118      * finished so we don't minimize the task if the transition fails.
119      */
120     private fun markTaskMinimized(displayId: Int, taskId: Int) {
121         KtProtoLog.v(
122                 ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
123                 "DesktopTasksLimiter: marking %d as minimized", taskId)
124         taskRepository.minimizeTask(displayId, taskId)
125     }
126 
127     /**
128      * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task
129      * limit.
130      *
131      * @param transition the transition that the minimize-transition will be appended to, or null if
132      * the transition will be started later.
133      * @return the ID of the minimized task, or null if no task is being minimized.
134      */
135     fun addAndGetMinimizeTaskChangesIfNeeded(
136             displayId: Int,
137             wct: WindowContainerTransaction,
138             newFrontTaskInfo: RunningTaskInfo,
139     ): RunningTaskInfo? {
140         KtProtoLog.v(
141                 ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
142                 "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d",
143                 newFrontTaskInfo.taskId)
144         val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront(
145                 taskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId),
146                 newFrontTaskInfo.taskId)
147         val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack)
148         if (taskToMinimize != null) {
149             wct.reorder(taskToMinimize.token, false /* onTop */)
150             return taskToMinimize
151         }
152         return null
153     }
154 
155     /**
156      * Add a pending minimize transition change, to update the list of minimized apps once the
157      * transition goes through.
158      */
159     fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) {
160         minimizeTransitionObserver.addPendingTransitionToken(
161                 transition, TaskDetails(displayId, taskId))
162     }
163 
164     /**
165      * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop
166      * Mode.
167      */
168     fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit()
169 
170     /**
171      * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and
172      * 2. a new task placed in front of all the others.
173      */
174     fun getTaskToMinimizeIfNeeded(
175             visibleFreeformTaskIdsOrderedFrontToBack: List<Int>,
176             newTaskIdInFront: Int
177     ): RunningTaskInfo? {
178         return getTaskToMinimizeIfNeeded(
179                 createOrderedTaskListWithGivenTaskInFront(
180                         visibleFreeformTaskIdsOrderedFrontToBack, newTaskIdInFront))
181     }
182 
183     /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */
184     fun getTaskToMinimizeIfNeeded(
185             visibleFreeformTaskIdsOrderedFrontToBack: List<Int>
186     ): RunningTaskInfo? {
187         if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) {
188             KtProtoLog.v(
189                     ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
190                     "DesktopTasksLimiter: no need to minimize; tasks below limit")
191             // No need to minimize anything
192             return null
193         }
194         val taskToMinimize =
195                 shellTaskOrganizer.getRunningTaskInfo(
196                         visibleFreeformTaskIdsOrderedFrontToBack.last())
197         if (taskToMinimize == null) {
198             KtProtoLog.e(
199                     ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
200                     "DesktopTasksLimiter: taskToMinimize == null")
201             return null
202         }
203         return taskToMinimize
204     }
205 
206     private fun createOrderedTaskListWithGivenTaskInFront(
207             existingTaskIdsOrderedFrontToBack: List<Int>,
208             newTaskId: Int
209     ): List<Int> {
210         return listOf(newTaskId) +
211                 existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId }
212     }
213 
214     @VisibleForTesting
215     fun getTransitionObserver(): TransitionObserver {
216         return minimizeTransitionObserver
217     }
218 }