• 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.recents
18 
19 import android.app.ActivityManager.RunningTaskInfo
20 import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
21 import android.os.IBinder
22 import android.util.ArrayMap
23 import android.view.SurfaceControl
24 import android.view.WindowManager.TRANSIT_CHANGE
25 import android.window.DesktopModeFlags
26 import android.window.TransitionInfo
27 import com.android.internal.protolog.ProtoLog
28 import com.android.wm.shell.Flags.enableShellTopTaskTracking
29 import com.android.wm.shell.ShellTaskOrganizer
30 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER
31 import com.android.wm.shell.shared.TransitionUtil
32 import com.android.wm.shell.sysui.ShellCommandHandler
33 import com.android.wm.shell.sysui.ShellInit
34 import com.android.wm.shell.transition.Transitions
35 import dagger.Lazy
36 import java.io.PrintWriter
37 import java.util.StringJoiner
38 import java.util.concurrent.Executor
39 
40 /**
41  * A [Transitions.TransitionObserver] that observes shell transitions, tracks the visible tasks
42  * and notifies listeners whenever the visible tasks change (at the start and end of a transition).
43  *
44  * This can be replaced once we have a generalized task repository tracking visible tasks.
45  */
46 class TaskStackTransitionObserver(
47     shellInit: ShellInit,
48     private val shellTaskOrganizer: Lazy<ShellTaskOrganizer>,
49     private val shellCommandHandler: ShellCommandHandler,
50     private val transitions: Lazy<Transitions>,
51 ) : Transitions.TransitionObserver, ShellTaskOrganizer.TaskVanishedListener {
52 
53     // List of currently visible tasks sorted in z-order from top-most to bottom-most, only used
54     // when Flags.enableShellTopTaskTracking() is enabled.
55     private var visibleTasks: MutableList<RunningTaskInfo> = mutableListOf()
56     private val pendingCloseTasks: MutableList<RunningTaskInfo> = mutableListOf()
57     // Set of listeners to notify when the visible tasks change
58     private val taskStackTransitionObserverListeners =
59         ArrayMap<TaskStackTransitionObserverListener, Executor>()
60     // Used to filter out leaf-tasks
61     private val leafTaskFilter: TransitionUtil.LeafTaskFilter = TransitionUtil.LeafTaskFilter()
62 
63     init {
64         shellInit.addInitCallback(::onInit, this)
65     }
66 
67     fun onInit() {
68         shellTaskOrganizer.get().addTaskVanishedListener(this)
69         shellCommandHandler.addDumpCallback(::dump, this)
70         transitions.get().registerObserver(this)
71 
72         // TODO(346588978): We need to update the running tasks once the ShellTaskOrganizer is
73         // registered since there is no existing transition (yet) corresponding for the already
74         // visible tasks
75     }
76 
77     /**
78      * This method handles transition ready when only
79      * DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL is set.
80      */
81     private fun onDesktopOnlyFlagTransitionReady(info: TransitionInfo) {
82         for (change in info.changes) {
83             if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
84                 continue
85             }
86 
87             val taskInfo = change.taskInfo
88             if (taskInfo == null || taskInfo.taskId == -1) {
89                 continue
90             }
91 
92             // Find the first task that is opening, this should be the one at the front after
93             // the transition
94             if (TransitionUtil.isOpeningType(change.mode)) {
95                 notifyOnTaskMovedToFront(taskInfo)
96                 break
97             } else if (change.mode == TRANSIT_CHANGE) {
98                 notifyOnTaskChanged(taskInfo)
99             }
100         }
101     }
102 
103     /**
104      * This method handles transition ready when Flags.enableShellTopTaskTracking() is set.
105      */
106     private fun onShellTopTaskTrackerFlagTransitionReady(info: TransitionInfo) {
107         ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Transition ready: %d", info.debugId)
108 
109         // Filter out non-leaf tasks (we will likely need them later, but visible task tracking
110         // is currently used only for visible leaf tasks)
111         val changesReversed = mutableListOf<TransitionInfo.Change>()
112         for (change in info.changes) {
113             if (!leafTaskFilter.test(change)) {
114                 // Not a leaf task
115                 continue
116             }
117             changesReversed.add(0, change)
118         }
119 
120         // We iterate the change list in reverse order because changes are sorted top to bottom and
121         // we want to update the lists such that the top most tasks are inserted at the front last
122         var notifyChanges = false
123         for (change in changesReversed) {
124             val taskInfo = change.taskInfo
125             if (taskInfo == null || taskInfo.taskId == -1) {
126                 // Not a valid task
127                 continue
128             }
129 
130             if (TransitionUtil.isClosingMode(change.mode)) {
131                 ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tClosing task=%d", taskInfo.taskId)
132 
133                 // Closing task's visibilities are not committed until after the transition
134                 // completes, so track such tasks so that we can notify on finish
135                 if (!pendingCloseTasks.any { it.taskId == taskInfo.taskId }) {
136                     pendingCloseTasks.add(taskInfo)
137                 }
138             } else if (TransitionUtil.isOpeningMode(change.mode)
139                     || TransitionUtil.isOrderOnly(change)) {
140                 ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tOpening task=%d", taskInfo.taskId)
141 
142                 // Remove from pending close tasks list if it's being opened again
143                 pendingCloseTasks.removeIf { it.taskId == taskInfo.taskId }
144                 // Move the task to the front of the visible tasks list
145                 visibleTasks.removeIf { it.taskId == taskInfo.taskId }
146                 visibleTasks.add(0, taskInfo)
147                 notifyChanges = true
148             }
149         }
150 
151         // TODO(346588978): We should verify the task list has actually changed before notifying
152         //  (ie. starting an activity that's already top-most would result in no visible change)
153         if (notifyChanges) {
154             updateVisibleTasksList("transition-start")
155         }
156     }
157 
158     private fun updateVisibleTasksList(reason: String) {
159         // This simply constructs a list of visible tasks, where the always-on-top tasks are moved
160         // to the front of the list in-order, to ensure that they match the visible z order
161         val orderedVisibleTasks = mutableListOf<RunningTaskInfo>()
162         var numAlwaysOnTop = 0
163         for (info in visibleTasks) {
164             if (info.windowingMode == WINDOWING_MODE_PINNED
165                     || info.configuration.windowConfiguration.isAlwaysOnTop) {
166                 orderedVisibleTasks.add(numAlwaysOnTop, info)
167                 numAlwaysOnTop++
168             } else {
169                 orderedVisibleTasks.add(info)
170             }
171         }
172         visibleTasks = orderedVisibleTasks
173 
174         dumpVisibleTasks(reason)
175         notifyVisibleTasksChanged(visibleTasks)
176     }
177 
178     override fun onTransitionReady(
179         transition: IBinder,
180         info: TransitionInfo,
181         startTransaction: SurfaceControl.Transaction,
182         finishTransaction: SurfaceControl.Transaction
183     ) {
184         if (enableShellTopTaskTracking()) {
185             onShellTopTaskTrackerFlagTransitionReady(info)
186         } else if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) {
187             onDesktopOnlyFlagTransitionReady(info)
188         }
189     }
190 
191     override fun onTransitionStarting(transition: IBinder) {}
192 
193     override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
194 
195     override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
196         if (!enableShellTopTaskTracking()) {
197             return
198         }
199 
200         if (pendingCloseTasks.isNotEmpty()) {
201             // Update the visible task list based on the pending close tasks
202             for (change in pendingCloseTasks) {
203                 visibleTasks.removeIf {
204                     it.taskId == change.taskId
205                 }
206             }
207             updateVisibleTasksList("transition-finished")
208         }
209     }
210 
211     override fun onTaskVanished(taskInfo: RunningTaskInfo?) {
212         if (!enableShellTopTaskTracking()) {
213             return
214         }
215         ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Task vanished: task=%d", taskInfo?.taskId)
216         pendingCloseTasks.removeIf { it.taskId == taskInfo?.taskId }
217         if (visibleTasks.any { it.taskId == taskInfo?.taskId }) {
218             visibleTasks.removeIf { it.taskId == taskInfo?.taskId }
219             updateVisibleTasksList("task-vanished")
220         }
221     }
222 
223     /**
224      * Adds a new task stack observer.
225      */
226     fun addTaskStackTransitionObserverListener(
227         taskStackTransitionObserverListener: TaskStackTransitionObserverListener,
228         executor: Executor
229     ) {
230         taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor
231     }
232 
233     /**
234      * Removes an existing task stack observer.
235      */
236     fun removeTaskStackTransitionObserverListener(
237         taskStackTransitionObserverListener: TaskStackTransitionObserverListener
238     ) {
239         taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener)
240     }
241 
242     private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) {
243         if (enableShellTopTaskTracking()) {
244             return
245         }
246         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
247             executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
248         }
249     }
250 
251     private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) {
252         if (enableShellTopTaskTracking()) {
253             return
254         }
255         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
256             executor.execute { listener.onTaskChangedThroughTransition(taskInfo) }
257         }
258     }
259 
260     private fun notifyVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {
261         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
262             executor.execute { listener.onVisibleTasksChanged(visibleTasks) }
263         }
264     }
265 
266     fun dump(pw: PrintWriter, prefix: String) {
267         pw.println("${prefix}$TAG:")
268 
269         if (visibleTasks.isEmpty()) {
270             pw.println("$prefix  visibleTasks=[]")
271         } else {
272             val stringJoiner = StringJoiner(",\n\t", "[\n\t", "\n]")
273             visibleTasks.forEach {
274                 stringJoiner.add("id=${it.taskId} cmp=${it.baseIntent.component}")
275             }
276             pw.println("$prefix  visibleTasks=$stringJoiner")
277         }
278     }
279 
280     /** Dumps the set of visible tasks to protolog */
281     private fun dumpVisibleTasks(reason: String) {
282         if (!WM_SHELL_TASK_OBSERVER.isEnabled) {
283             return
284         }
285         ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tVisible tasks (%s)", reason)
286         for (task in visibleTasks) {
287             ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\t\ttaskId=%d package=%s", task.taskId,
288                 task.baseIntent.component?.packageName)
289         }
290     }
291 
292     /** Listener to use to get updates regarding task stack from this observer */
293     interface TaskStackTransitionObserverListener {
294         /** Called when a task is moved to front. */
295         fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
296         /** Called when the set of visible tasks have changed. */
297         fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {}
298         /** Called when a task info has changed. */
299         fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {}
300     }
301 
302     companion object {
303         const val TAG = "TaskStackTransitionObserver"
304     }
305 }
306