• 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.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