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.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 21 import android.content.Context 22 import android.os.IBinder 23 import android.view.SurfaceControl 24 import android.view.WindowManager.TRANSIT_CLOSE 25 import android.view.WindowManager.TRANSIT_OPEN 26 import android.view.WindowManager.TRANSIT_TO_BACK 27 import android.window.DesktopExperienceFlags 28 import android.window.DesktopModeFlags 29 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER 30 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY 31 import android.window.TransitionInfo 32 import android.window.WindowContainerTransaction 33 import com.android.internal.protolog.ProtoLog 34 import com.android.wm.shell.ShellTaskOrganizer 35 import com.android.wm.shell.back.BackAnimationController 36 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition 37 import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider 38 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 39 import com.android.wm.shell.shared.TransitionUtil 40 import com.android.wm.shell.shared.TransitionUtil.isClosingMode 41 import com.android.wm.shell.shared.TransitionUtil.isOpeningMode 42 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus 43 import com.android.wm.shell.sysui.ShellInit 44 import com.android.wm.shell.transition.Transitions 45 46 /** 47 * A [Transitions.TransitionObserver] that observes shell transitions and updates the 48 * [DesktopRepository] state TODO: b/332682201 This observes transitions related to desktop mode and 49 * other transitions that originate both within and outside shell. 50 */ 51 class DesktopTasksTransitionObserver( 52 private val context: Context, 53 private val desktopUserRepositories: DesktopUserRepositories, 54 private val transitions: Transitions, 55 private val shellTaskOrganizer: ShellTaskOrganizer, 56 private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, 57 private val backAnimationController: BackAnimationController, 58 private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, 59 shellInit: ShellInit, 60 ) : Transitions.TransitionObserver { 61 62 data class CloseWallpaperTransition(val transition: IBinder, val displayId: Int) 63 64 private var transitionToCloseWallpaper: CloseWallpaperTransition? = null 65 private var currentProfileId: Int 66 67 init { 68 if (DesktopModeStatus.canEnterDesktopMode(context)) { 69 shellInit.addInitCallback(::onInit, this) 70 } 71 currentProfileId = ActivityManager.getCurrentUser() 72 } 73 74 fun onInit() { 75 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTasksTransitionObserver: onInit") 76 transitions.registerObserver(this) 77 } 78 79 override fun onTransitionReady( 80 transition: IBinder, 81 info: TransitionInfo, 82 startTransaction: SurfaceControl.Transaction, 83 finishTransaction: SurfaceControl.Transaction, 84 ) { 85 // TODO: b/332682201 Update repository state 86 if ( 87 DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC 88 .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() 89 ) { 90 updateTopTransparentFullscreenTaskId(info) 91 } 92 updateWallpaperToken(info) 93 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { 94 handleBackNavigation(transition, info) 95 removeTaskIfNeeded(info) 96 } 97 removeWallpaperOnLastTaskClosingIfNeeded(transition, info) 98 } 99 100 private fun removeTaskIfNeeded(info: TransitionInfo) { 101 // Since we are no longer removing all the tasks [onTaskVanished], we need to remove them by 102 // checking the transitions. 103 if (!(TransitionUtil.isOpeningType(info.type) || info.type.isExitDesktopModeTransition())) { 104 return 105 } 106 // Remove a task from the repository if the app is launched outside of desktop. 107 for (change in info.changes) { 108 val taskInfo = change.taskInfo 109 if (taskInfo == null || taskInfo.taskId == -1) continue 110 111 val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) 112 if ( 113 desktopRepository.isActiveTask(taskInfo.taskId) && 114 taskInfo.windowingMode != WINDOWING_MODE_FREEFORM 115 ) { 116 desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId) 117 } 118 } 119 } 120 121 private fun handleBackNavigation(transition: IBinder, info: TransitionInfo) { 122 // When default back navigation happens, transition type is TO_BACK and the change is 123 // TO_BACK. Mark the task going to back as minimized. 124 if (info.type == TRANSIT_TO_BACK) { 125 for (change in info.changes) { 126 val taskInfo = change.taskInfo 127 if (taskInfo == null || taskInfo.taskId == -1) { 128 continue 129 } 130 val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) 131 val isInDesktop = desktopRepository.isAnyDeskActive(taskInfo.displayId) 132 if ( 133 isInDesktop && 134 change.mode == TRANSIT_TO_BACK && 135 taskInfo.windowingMode == WINDOWING_MODE_FREEFORM 136 ) { 137 val isLastTask = 138 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 139 desktopRepository.hasOnlyOneVisibleTask(taskInfo.displayId) 140 } else { 141 desktopRepository.isOnlyVisibleTask(taskInfo.taskId, taskInfo.displayId) 142 } 143 desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) 144 desktopMixedTransitionHandler.addPendingMixedTransition( 145 DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( 146 transition, 147 taskInfo.taskId, 148 isLastTask, 149 ) 150 ) 151 } 152 } 153 } else if (info.type == TRANSIT_CLOSE) { 154 // In some cases app will be closing as a result of back navigation but we would like 155 // to minimize. Mark the task closing as minimized. 156 var hasWallpaperClosing = false 157 var minimizingTask: Int? = null 158 for (change in info.changes) { 159 val taskInfo = change.taskInfo 160 if (taskInfo == null || taskInfo.taskId == -1) continue 161 162 if ( 163 TransitionUtil.isClosingMode(change.mode) && 164 DesktopWallpaperActivity.isWallpaperTask(taskInfo) 165 ) { 166 hasWallpaperClosing = true 167 } 168 169 if (change.mode == TRANSIT_CLOSE && minimizingTask == null) { 170 minimizingTask = getMinimizingTaskForClosingTransition(taskInfo) 171 } 172 } 173 174 if (minimizingTask == null) return 175 // If the transition has wallpaper closing, it means we are moving out of desktop. 176 desktopMixedTransitionHandler.addPendingMixedTransition( 177 DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( 178 transition, 179 minimizingTask, 180 isLastTask = hasWallpaperClosing, 181 ) 182 ) 183 } 184 } 185 186 /** 187 * Given this a closing task in a closing transition, a task is assumed to be closed by back 188 * navigation if: 189 * 1) Desktop mode is visible. 190 * 2) Task is in freeform. 191 * 3) Task is the latest task that the back gesture is triggered on. 192 * 4) It's not marked as a closing task as a result of closing it by the app header. 193 * 194 * This doesn't necessarily mean all the cases are because of back navigation but those cases 195 * will be rare. E.g. triggering back navigation on an app that pops up a close dialog, and 196 * closing it will minimize it here. 197 */ 198 private fun getMinimizingTaskForClosingTransition( 199 taskInfo: ActivityManager.RunningTaskInfo 200 ): Int? { 201 val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) 202 val isInDesktop = desktopRepository.isAnyDeskActive(taskInfo.displayId) 203 if ( 204 isInDesktop && 205 taskInfo.windowingMode == WINDOWING_MODE_FREEFORM && 206 backAnimationController.latestTriggerBackTask == taskInfo.taskId && 207 !desktopRepository.isClosingTask(taskInfo.taskId) 208 ) { 209 desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) 210 return taskInfo.taskId 211 } 212 return null 213 } 214 215 private fun removeWallpaperOnLastTaskClosingIfNeeded( 216 transition: IBinder, 217 info: TransitionInfo, 218 ) { 219 // TODO: 380868195 - Smooth animation for wallpaper activity closing just by itself 220 for (change in info.changes) { 221 val taskInfo = change.taskInfo 222 if (taskInfo == null || taskInfo.taskId == -1) { 223 continue 224 } 225 226 val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) 227 if ( 228 !desktopRepository.isAnyDeskActive(taskInfo.displayId) && 229 change.mode == TRANSIT_CLOSE && 230 taskInfo.windowingMode == WINDOWING_MODE_FREEFORM && 231 desktopWallpaperActivityTokenProvider.getToken(taskInfo.displayId) != null 232 ) { 233 transitionToCloseWallpaper = 234 CloseWallpaperTransition(transition, taskInfo.displayId) 235 currentProfileId = taskInfo.userId 236 } 237 } 238 } 239 240 override fun onTransitionStarting(transition: IBinder) { 241 // TODO: b/332682201 Update repository state 242 } 243 244 override fun onTransitionMerged(merged: IBinder, playing: IBinder) { 245 // TODO: b/332682201 Update repository state 246 } 247 248 override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { 249 val lastSeenTransitionToCloseWallpaper = transitionToCloseWallpaper 250 // TODO: b/332682201 Update repository state 251 if (lastSeenTransitionToCloseWallpaper?.transition == transition) { 252 // TODO: b/362469671 - Handle merging the animation when desktop is also closing. 253 desktopWallpaperActivityTokenProvider 254 .getToken(lastSeenTransitionToCloseWallpaper.displayId) 255 ?.let { wallpaperActivityToken -> 256 if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { 257 transitions.startTransition( 258 TRANSIT_TO_BACK, 259 WindowContainerTransaction() 260 .reorder(wallpaperActivityToken, /* onTop= */ false), 261 null, 262 ) 263 } else { 264 transitions.startTransition( 265 TRANSIT_CLOSE, 266 WindowContainerTransaction().removeTask(wallpaperActivityToken), 267 null, 268 ) 269 } 270 } 271 transitionToCloseWallpaper = null 272 } 273 } 274 275 private fun updateWallpaperToken(info: TransitionInfo) { 276 if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { 277 return 278 } 279 info.changes.forEach { change -> 280 change.taskInfo?.let { taskInfo -> 281 if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { 282 when (change.mode) { 283 TRANSIT_OPEN -> { 284 desktopWallpaperActivityTokenProvider.setToken( 285 taskInfo.token, 286 taskInfo.displayId, 287 ) 288 // After the task for the wallpaper is created, set it non-trimmable. 289 // This is important to prevent recents from trimming and removing the 290 // task. 291 shellTaskOrganizer.applyTransaction( 292 WindowContainerTransaction() 293 .setTaskTrimmableFromRecents(taskInfo.token, false) 294 ) 295 } 296 TRANSIT_CLOSE -> 297 desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId) 298 else -> {} 299 } 300 } 301 } 302 } 303 } 304 305 private fun updateTopTransparentFullscreenTaskId(info: TransitionInfo) { 306 run forEachLoop@{ 307 info.changes.forEach { change -> 308 change.taskInfo?.let { task -> 309 val desktopRepository = desktopUserRepositories.getProfile(task.userId) 310 val displayId = task.displayId 311 val transparentTaskId = 312 desktopRepository.getTopTransparentFullscreenTaskId(displayId) 313 if (transparentTaskId == null) return@forEachLoop 314 val changeMode = change.mode 315 val taskId = task.taskId 316 val isTopTransparentFullscreenTaskClosing = 317 taskId == transparentTaskId && isClosingMode(changeMode) 318 val isNonTopTransparentFullscreenTaskOpening = 319 taskId != transparentTaskId && isOpeningMode(changeMode) 320 // Clear `topTransparentFullscreenTask` information from repository if task 321 // is closed, sent to back or if a different task is opened, brought to front. 322 if ( 323 isTopTransparentFullscreenTaskClosing || 324 isNonTopTransparentFullscreenTaskOpening 325 ) { 326 desktopRepository.clearTopTransparentFullscreenTaskId(displayId) 327 return@forEachLoop 328 } 329 } 330 } 331 } 332 } 333 } 334