1 /* <lambda>null2 * Copyright (C) 2025 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 package com.android.wm.shell.desktopmode.multidesks 17 18 import android.os.IBinder 19 import android.view.WindowManager.TRANSIT_CLOSE 20 import android.window.DesktopExperienceFlags 21 import android.window.TransitionInfo 22 import com.android.internal.protolog.ProtoLog 23 import com.android.wm.shell.desktopmode.DesktopUserRepositories 24 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 25 26 /** 27 * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It 28 * tracks pending transitions and updates repository state once they finish. 29 */ 30 class DesksTransitionObserver( 31 private val desktopUserRepositories: DesktopUserRepositories, 32 private val desksOrganizer: DesksOrganizer, 33 ) { 34 private val deskTransitions = mutableMapOf<IBinder, MutableSet<DeskTransition>>() 35 36 /** Adds a pending desk transition to be tracked. */ 37 fun addPendingTransition(transition: DeskTransition) { 38 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return 39 val transitions = deskTransitions[transition.token] ?: mutableSetOf() 40 transitions += transition 41 deskTransitions[transition.token] = transitions 42 logD("Added pending desk transition: %s", transition) 43 } 44 45 /** 46 * Called when any transition is ready, which may include transitions not tracked by this 47 * observer. 48 */ 49 fun onTransitionReady(transition: IBinder, info: TransitionInfo) { 50 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return 51 val deskTransitions = deskTransitions.remove(transition) ?: return 52 deskTransitions.forEach { deskTransition -> handleDeskTransition(info, deskTransition) } 53 } 54 55 /** 56 * Called when a transition is merged with another transition, which may include transitions not 57 * tracked by this observer. 58 */ 59 fun onTransitionMerged(merged: IBinder, playing: IBinder) { 60 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return 61 val transitions = deskTransitions.remove(merged) ?: return 62 deskTransitions[playing] = 63 transitions 64 .map { deskTransition -> deskTransition.copyWithToken(token = playing) } 65 .toMutableSet() 66 } 67 68 /** 69 * Called when any transition finishes, which may include transitions not tracked by this 70 * observer. 71 * 72 * Most [DeskTransition]s are not handled here because [onTransitionReady] handles them and 73 * removes them from the map. However, there can be cases where the transition was added after 74 * [onTransitionReady] had already been called and they need to be handled here, such as the 75 * swipe-to-home recents transition when there is no book-end transition. 76 */ 77 fun onTransitionFinished(transition: IBinder) { 78 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return 79 val deskTransitions = deskTransitions.remove(transition) ?: return 80 deskTransitions.forEach { deskTransition -> 81 if (deskTransition is DeskTransition.DeactivateDesk) { 82 handleDeactivateDeskTransition(null, deskTransition) 83 } else { 84 logW( 85 "Unexpected desk transition finished without being handled: %s", 86 deskTransition, 87 ) 88 } 89 } 90 } 91 92 private fun handleDeskTransition(info: TransitionInfo, deskTransition: DeskTransition) { 93 logD("Desk transition ready: %s", deskTransition) 94 val desktopRepository = desktopUserRepositories.current 95 when (deskTransition) { 96 is DeskTransition.RemoveDesk -> { 97 check(info.type == TRANSIT_CLOSE) { "Expected close transition for desk removal" } 98 // TODO: b/362720497 - consider verifying the desk was actually removed through the 99 // DesksOrganizer. The transition info won't have changes if the desk was not 100 // visible, such as when dismissing from Overview. 101 val deskId = deskTransition.deskId 102 val displayId = deskTransition.displayId 103 desktopRepository.removeDesk(deskTransition.deskId) 104 deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId) 105 } 106 is DeskTransition.ActivateDesk -> { 107 val activateDeskChange = 108 info.changes.find { change -> 109 desksOrganizer.isDeskActiveAtEnd(change, deskTransition.deskId) 110 } 111 if (activateDeskChange == null) { 112 // Always activate even if there is no change in the transition for the 113 // activated desk. This is necessary because some activation requests, such as 114 // those involving empty desks, may not contain visibility changes that are 115 // reported in the transition change list. 116 logD("Activating desk without transition change") 117 } 118 desktopRepository.setActiveDesk( 119 displayId = deskTransition.displayId, 120 deskId = deskTransition.deskId, 121 ) 122 } 123 is DeskTransition.ActiveDeskWithTask -> { 124 val withTask = 125 info.changes.find { change -> 126 change.taskInfo?.taskId == deskTransition.enterTaskId && 127 change.taskInfo?.isVisibleRequested == true && 128 desksOrganizer.getDeskAtEnd(change) == deskTransition.deskId 129 } 130 withTask?.let { 131 desktopRepository.setActiveDesk( 132 displayId = deskTransition.displayId, 133 deskId = deskTransition.deskId, 134 ) 135 desktopRepository.addTaskToDesk( 136 displayId = deskTransition.displayId, 137 deskId = deskTransition.deskId, 138 taskId = deskTransition.enterTaskId, 139 isVisible = true, 140 ) 141 } 142 } 143 is DeskTransition.DeactivateDesk -> handleDeactivateDeskTransition(info, deskTransition) 144 } 145 } 146 147 private fun handleDeactivateDeskTransition( 148 info: TransitionInfo?, 149 deskTransition: DeskTransition.DeactivateDesk, 150 ) { 151 logD("handleDeactivateDeskTransition: %s", deskTransition) 152 val desktopRepository = desktopUserRepositories.current 153 var deskChangeFound = false 154 155 val changes = info?.changes ?: emptyList() 156 for (change in changes) { 157 val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) 158 if (isDeskChange) { 159 deskChangeFound = true 160 continue 161 } 162 val taskId = change.taskInfo?.taskId ?: continue 163 val removedFromDesk = 164 desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId && 165 desksOrganizer.getDeskAtEnd(change) == null 166 if (removedFromDesk) { 167 desktopRepository.removeTaskFromDesk( 168 deskId = deskTransition.deskId, 169 taskId = taskId, 170 ) 171 } 172 } 173 // Always deactivate even if there's no change that confirms the desk was 174 // deactivated. Some interactions, such as the desk deactivating because it's 175 // occluded by a fullscreen task result in a transition change, but others, such 176 // as transitioning from an empty desk to home may not. 177 if (!deskChangeFound) { 178 logD("Deactivating desk without transition change") 179 } 180 desktopRepository.setDeskInactive(deskId = deskTransition.deskId) 181 } 182 183 private fun logD(msg: String, vararg arguments: Any?) { 184 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 185 } 186 187 private fun logW(msg: String, vararg arguments: Any?) { 188 ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 189 } 190 191 private companion object { 192 private const val TAG = "DesksTransitionObserver" 193 } 194 } 195