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