• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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