• 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.wm.shell.desktopmode
18 
19 import android.app.ActivityManager
20 import android.content.Context
21 import android.os.Handler
22 import android.os.IBinder
23 import android.view.SurfaceControl
24 import android.view.WindowManager.TRANSIT_TO_BACK
25 import android.window.DesktopExperienceFlags
26 import android.window.DesktopModeFlags
27 import android.window.TransitionInfo
28 import android.window.WindowContainerTransaction
29 import androidx.annotation.VisibleForTesting
30 import com.android.internal.jank.InteractionJankMonitor
31 import com.android.internal.protolog.ProtoLog
32 import com.android.wm.shell.ShellTaskOrganizer
33 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
34 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
35 import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
36 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
37 import com.android.wm.shell.shared.annotations.ShellMainThread
38 import com.android.wm.shell.sysui.UserChangeListener
39 import com.android.wm.shell.transition.Transitions
40 import com.android.wm.shell.transition.Transitions.TransitionObserver
41 
42 /**
43  * Keeps track of minimized tasks and limits the number of tasks shown in Desktop Mode.
44  *
45  * [maxTasksLimit] must be strictly greater than 0 if it's given.
46  *
47  * TODO(b/400634379): Separate two responsibilities of this class into two classes.
48  */
49 class DesktopTasksLimiter(
50     transitions: Transitions,
51     private val desktopUserRepositories: DesktopUserRepositories,
52     private val shellTaskOrganizer: ShellTaskOrganizer,
53     private val desksOrganizer: DesksOrganizer,
54     private val maxTasksLimit: Int?,
55     private val interactionJankMonitor: InteractionJankMonitor,
56     private val context: Context,
57     @ShellMainThread private val handler: Handler,
58 ) {
59     private val minimizeTransitionObserver = MinimizeTransitionObserver()
60     @VisibleForTesting val leftoverMinimizedTasksRemover = LeftoverMinimizedTasksRemover()
61 
62     private var userId: Int
63 
64     init {
65         maxTasksLimit?.let {
66             require(it > 0) {
67                 "DesktopTasksLimiter: maxTasksLimit should be greater than 0. Current value: $it."
68             }
69         }
70         transitions.registerObserver(minimizeTransitionObserver)
71         userId = ActivityManager.getCurrentUser()
72         desktopUserRepositories.current.addActiveTaskListener(leftoverMinimizedTasksRemover)
73         if (maxTasksLimit != null) {
74             logV("Starting limiter with a maximum of %d tasks", maxTasksLimit)
75         } else {
76             logV("Starting limiter without the task limit")
77         }
78     }
79 
80     data class TaskDetails(
81         val displayId: Int,
82         val taskId: Int,
83         var transitionInfo: TransitionInfo? = null,
84         val minimizeReason: MinimizeReason? = null,
85         val unminimizeReason: UnminimizeReason? = null,
86     )
87 
88     /**
89      * Returns the task being minimized in the given transition if that transition is a pending or
90      * active minimize transition.
91      */
92     fun getMinimizingTask(transition: IBinder): TaskDetails? {
93         return minimizeTransitionObserver.getMinimizingTask(transition)
94     }
95 
96     /**
97      * Returns the task being unminimized in the given transition if that transition is a pending or
98      * active unminimize transition.
99      */
100     fun getUnminimizingTask(transition: IBinder): TaskDetails? {
101         return minimizeTransitionObserver.getUnminimizingTask(transition)
102     }
103 
104     // TODO(b/333018485): replace this observer when implementing the minimize-animation
105     private inner class MinimizeTransitionObserver : TransitionObserver {
106         private val pendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
107         private val activeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
108         private val pendingUnminimizeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
109         private val activeUnminimizeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
110 
111         fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) {
112             pendingTransitionTokensAndTasks[transition] = taskDetails
113         }
114 
115         fun addPendingUnminimizeTransitionToken(transition: IBinder, taskDetails: TaskDetails) {
116             pendingUnminimizeTransitionTokensAndTasks[transition] = taskDetails
117         }
118 
119         fun getMinimizingTask(transition: IBinder): TaskDetails? {
120             return pendingTransitionTokensAndTasks[transition]
121                 ?: activeTransitionTokensAndTasks[transition]
122         }
123 
124         fun getUnminimizingTask(transition: IBinder): TaskDetails? {
125             return pendingUnminimizeTransitionTokensAndTasks[transition]
126                 ?: activeUnminimizeTransitionTokensAndTasks[transition]
127         }
128 
129         override fun onTransitionReady(
130             transition: IBinder,
131             info: TransitionInfo,
132             startTransaction: SurfaceControl.Transaction,
133             finishTransaction: SurfaceControl.Transaction,
134         ) {
135             val taskRepository = desktopUserRepositories.current
136             handleMinimizeTransitionReady(taskRepository, transition, info)
137             handleUnminimizeTransitionReady(transition)
138         }
139 
140         private fun handleMinimizeTransitionReady(
141             taskRepository: DesktopRepository,
142             transition: IBinder,
143             info: TransitionInfo,
144         ) {
145             val taskToMinimize = pendingTransitionTokensAndTasks.remove(transition) ?: return
146             if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return
147             if (!isTaskReadyForMinimize(info, taskToMinimize)) {
148                 logV("task %d is not reordered to back nor invis", taskToMinimize.taskId)
149                 return
150             }
151             taskToMinimize.transitionInfo = info
152             activeTransitionTokensAndTasks[transition] = taskToMinimize
153 
154             // Save current bounds before minimizing in case we need to restore to it later.
155             val boundsBeforeMinimize =
156                 info.changes
157                     .find { change -> change.taskInfo?.taskId == taskToMinimize.taskId }
158                     ?.startAbsBounds
159             taskRepository.saveBoundsBeforeMinimize(taskToMinimize.taskId, boundsBeforeMinimize)
160 
161             this@DesktopTasksLimiter.minimizeTask(taskToMinimize.displayId, taskToMinimize.taskId)
162         }
163 
164         private fun handleUnminimizeTransitionReady(transition: IBinder) {
165             val taskToUnminimize =
166                 pendingUnminimizeTransitionTokensAndTasks.remove(transition) ?: return
167             activeUnminimizeTransitionTokensAndTasks[transition] = taskToUnminimize
168         }
169 
170         /**
171          * Returns whether the Task [taskDetails] is being reordered to the back in the transition
172          * [info], or is already invisible.
173          *
174          * This check confirms a task should be minimized before minimizing it.
175          */
176         private fun isTaskReadyForMinimize(
177             info: TransitionInfo,
178             taskDetails: TaskDetails,
179         ): Boolean {
180             val taskChange =
181                 info.changes.find { change -> change.taskInfo?.taskId == taskDetails.taskId }
182             val taskRepository = desktopUserRepositories.current
183             if (taskChange == null) return !taskRepository.isVisibleTask(taskDetails.taskId)
184             return taskChange.mode == TRANSIT_TO_BACK
185         }
186 
187         private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
188             info.changes.find { change ->
189                 change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK
190             }
191 
192         override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
193             activeTransitionTokensAndTasks.remove(merged)
194             pendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer ->
195                 pendingTransitionTokensAndTasks[playing] = taskToTransfer
196             }
197 
198             activeUnminimizeTransitionTokensAndTasks.remove(merged)
199             pendingUnminimizeTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer ->
200                 pendingUnminimizeTransitionTokensAndTasks[playing] = taskToTransfer
201             }
202         }
203 
204         override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
205             pendingTransitionTokensAndTasks.remove(transition)
206             activeUnminimizeTransitionTokensAndTasks.remove(transition)
207             pendingUnminimizeTransitionTokensAndTasks.remove(transition)
208         }
209     }
210 
211     @VisibleForTesting
212     inner class LeftoverMinimizedTasksRemover :
213         DesktopRepository.ActiveTasksListener, UserChangeListener {
214         override fun onActiveTasksChanged(displayId: Int) {
215             // If back navigation is enabled, we shouldn't remove the leftover tasks
216             if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return
217             val wct = WindowContainerTransaction()
218             removeLeftoverMinimizedTasks(displayId, wct)
219             shellTaskOrganizer.applyTransaction(wct)
220         }
221 
222         fun removeLeftoverMinimizedTasks(displayId: Int, wct: WindowContainerTransaction) {
223             val taskRepository = desktopUserRepositories.current
224             if (taskRepository.getExpandedTasksOrdered(displayId).isNotEmpty()) return
225             val remainingMinimizedTasks = taskRepository.getMinimizedTasks(displayId)
226             if (remainingMinimizedTasks.isEmpty()) return
227 
228             logV("Removing leftover minimized tasks: %s", remainingMinimizedTasks)
229             remainingMinimizedTasks.forEach { taskIdToRemove ->
230                 val taskToRemove = shellTaskOrganizer.getRunningTaskInfo(taskIdToRemove)
231                 if (taskToRemove != null) {
232                     wct.removeTask(taskToRemove.token)
233                 }
234             }
235         }
236 
237         override fun onUserChanged(newUserId: Int, userContext: Context) {
238             // Removes active task listener for the previous repository
239             desktopUserRepositories.getProfile(userId).removeActiveTasksListener(this)
240 
241             // Sets active listener for the current repository.
242             userId = newUserId
243             desktopUserRepositories.getProfile(newUserId).addActiveTaskListener(this)
244         }
245     }
246 
247     /**
248      * Mark task with [taskId] on [displayId] as minimized.
249      *
250      * This should be after the corresponding transition has finished so we don't minimize the task
251      * if the transition fails.
252      */
253     private fun minimizeTask(displayId: Int, taskId: Int) {
254         logV("Minimize taskId=%d, displayId=%d", taskId, displayId)
255         val taskRepository = desktopUserRepositories.current
256         taskRepository.minimizeTask(displayId, taskId)
257     }
258 
259     /**
260      * Adds a minimize-transition to [wct] if adding [newFrontTaskInfo] crosses task limit,
261      * returning the task to minimize.
262      */
263     fun addAndGetMinimizeTaskChanges(
264         deskId: Int,
265         wct: WindowContainerTransaction,
266         newFrontTaskId: Int?,
267         launchingNewIntent: Boolean = false,
268     ): Int? {
269         logV("addAndGetMinimizeTaskChanges, newFrontTask=%d", newFrontTaskId)
270         val taskRepository = desktopUserRepositories.current
271         val taskIdToMinimize =
272             getTaskIdToMinimize(
273                 taskRepository.getExpandedTasksIdsInDeskOrdered(deskId),
274                 newFrontTaskId,
275                 launchingNewIntent,
276             )
277         taskIdToMinimize
278             ?.let { shellTaskOrganizer.getRunningTaskInfo(it) }
279             ?.let { task ->
280                 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
281                     wct.reorder(task.token, /* onTop= */ false)
282                 } else {
283                     desksOrganizer.minimizeTask(wct, deskId, task)
284                 }
285             }
286         return taskIdToMinimize
287     }
288 
289     /**
290      * Add a pending minimize transition change to update the list of minimized apps once the
291      * transition goes through.
292      */
293     fun addPendingMinimizeChange(
294         transition: IBinder,
295         displayId: Int,
296         taskId: Int,
297         minimizeReason: MinimizeReason,
298     ) {
299         minimizeTransitionObserver.addPendingTransitionToken(
300             transition,
301             TaskDetails(displayId, taskId, transitionInfo = null, minimizeReason = minimizeReason),
302         )
303     }
304 
305     /**
306      * Add a pending unminimize transition change to allow tracking unminimizing transitions /
307      * tasks.
308      */
309     fun addPendingUnminimizeChange(
310         transition: IBinder,
311         displayId: Int,
312         taskId: Int,
313         unminimizeReason: UnminimizeReason,
314     ) =
315         minimizeTransitionObserver.addPendingUnminimizeTransitionToken(
316             transition,
317             TaskDetails(displayId, taskId, unminimizeReason = unminimizeReason),
318         )
319 
320     /**
321      * Returns the minimized task from the list of visible tasks ordered from front to back with the
322      * new task placed in front of other tasks.
323      */
324     fun getTaskIdToMinimize(
325         visibleOrderedTasks: List<Int>,
326         newTaskIdInFront: Int? = null,
327         launchingNewIntent: Boolean = false,
328     ): Int? {
329         return getTaskIdToMinimize(
330             createOrderedTaskListWithGivenTaskInFront(visibleOrderedTasks, newTaskIdInFront),
331             launchingNewIntent,
332         )
333     }
334 
335     /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */
336     private fun getTaskIdToMinimize(
337         visibleOrderedTasks: List<Int>,
338         launchingNewIntent: Boolean,
339     ): Int? {
340         val newTasksOpening = if (launchingNewIntent) 1 else 0
341         if (visibleOrderedTasks.size + newTasksOpening <= (maxTasksLimit ?: Int.MAX_VALUE)) {
342             logV("No need to minimize; tasks below limit")
343             // No need to minimize anything
344             return null
345         }
346         return visibleOrderedTasks.last()
347     }
348 
349     private fun createOrderedTaskListWithGivenTaskInFront(
350         existingTaskIdsOrderedFrontToBack: List<Int>,
351         newTaskId: Int?,
352     ): List<Int> {
353         return if (newTaskId == null) existingTaskIdsOrderedFrontToBack
354         else
355             listOf(newTaskId) +
356                 existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId }
357     }
358 
359     @VisibleForTesting fun getTransitionObserver(): TransitionObserver = minimizeTransitionObserver
360 
361     private fun logV(msg: String, vararg arguments: Any?) {
362         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
363     }
364 
365     private companion object {
366         const val TAG = "DesktopTasksLimiter"
367     }
368 }
369