• 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.RunningTaskInfo
20 import android.app.ActivityTaskManager.INVALID_TASK_ID
21 import android.app.TaskInfo
22 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
23 import android.content.Context
24 import android.os.IBinder
25 import android.os.SystemProperties
26 import android.os.Trace
27 import android.util.SparseArray
28 import android.view.SurfaceControl
29 import android.view.WindowManager
30 import android.view.WindowManager.TRANSIT_OPEN
31 import android.window.TransitionInfo
32 import android.window.TransitionInfo.FLAG_MOVED_TO_TOP
33 import androidx.annotation.VisibleForTesting
34 import androidx.core.util.containsKey
35 import androidx.core.util.forEach
36 import androidx.core.util.isEmpty
37 import androidx.core.util.isNotEmpty
38 import androidx.core.util.plus
39 import androidx.core.util.putAll
40 import com.android.internal.protolog.ProtoLog
41 import com.android.wm.shell.ShellTaskOrganizer
42 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
43 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
44 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.FocusReason
45 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
46 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate
47 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
48 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
49 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
50 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
51 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
52 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
53 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
54 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
55 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
56 import com.android.wm.shell.shared.TransitionUtil
57 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
58 import com.android.wm.shell.sysui.ShellInit
59 import com.android.wm.shell.transition.Transitions
60 import java.util.Optional
61 import kotlin.jvm.optionals.getOrNull
62 
63 /**
64  * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log
65  * appropriate desktop mode session log events. This observes transitions related to desktop mode
66  * and other transitions that originate both within and outside shell.
67  */
68 class DesktopModeLoggerTransitionObserver(
69     context: Context,
70     shellInit: ShellInit,
71     private val transitions: Transitions,
72     private val desktopModeEventLogger: DesktopModeEventLogger,
73     private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
74     private val shellTaskOrganizer: ShellTaskOrganizer,
75 ) : Transitions.TransitionObserver {
76 
77     init {
78         if (DesktopModeStatus.canEnterDesktopMode(context)) {
79             shellInit.addInitCallback(this::onInit, this)
80         }
81     }
82 
83     // A sparse array of visible freeform tasks and taskInfos
84     private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray()
85 
86     // Caching the taskInfos to handle canceled recents animations, if we identify that the recents
87     // animation was cancelled, we restore these tasks to calculate the post-Transition state
88     private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray()
89 
90     // Caching whether the previous transition was exit to overview.
91     private var wasPreviousTransitionExitToOverview: Boolean = false
92 
93     // Caching whether the previous transition was exit due to screen off. This helps check if a
94     // following enter reason could be Screen On
95     private var wasPreviousTransitionExitByScreenOff: Boolean = false
96 
97     private var focusedFreeformTask: TaskInfo? = null
98 
99     @VisibleForTesting var isSessionActive: Boolean = false
100 
101     fun onInit() {
102         transitions.registerObserver(this)
103         SystemProperties.set(
104             VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
105             VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE,
106         )
107         desktopModeEventLogger.logTaskInfoStateInit()
108     }
109 
110     override fun onTransitionReady(
111         transition: IBinder,
112         info: TransitionInfo,
113         startTransaction: SurfaceControl.Transaction,
114         finishTransaction: SurfaceControl.Transaction,
115     ) {
116         // this was a new recents animation
117         if (info.isExitToRecentsTransition() && tasksSavedForRecents.isEmpty()) {
118             ProtoLog.v(
119                 WM_SHELL_DESKTOP_MODE,
120                 "DesktopModeLogger: Recents animation running, saving tasks for later",
121             )
122             // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled
123             // recents animation
124 
125             // when recents animation is running, all freeform tasks are sent TO_BACK temporarily
126             // if the user ends up at home, we need to update the visible freeform tasks
127             // if the user cancels the animation, the subsequent transition is NONE
128             // if the user opens a new task, the subsequent transition is OPEN with flag
129             tasksSavedForRecents.putAll(visibleFreeformTaskInfos)
130         }
131 
132         // figure out what the new state of freeform tasks would be post transition
133         var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info)
134 
135         // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if
136         // that's the case, we might have accidentally logged a session exit and would need to
137         // revaluate again. Add all the tasks back.
138         // This will start a new desktop mode session.
139         if (
140             info.type == WindowManager.TRANSIT_NONE &&
141                 info.flags == 0 &&
142                 tasksSavedForRecents.isNotEmpty()
143         ) {
144             ProtoLog.v(
145                 WM_SHELL_DESKTOP_MODE,
146                 "DesktopModeLogger: Canceled recents animation, restoring tasks",
147             )
148             // restore saved tasks in the updated set and clear for next use
149             postTransitionVisibleFreeformTasks += tasksSavedForRecents
150             tasksSavedForRecents.clear()
151         }
152 
153         // identify if we need to log any changes and update the state of visible freeform tasks
154         identifyLogEventAndUpdateState(
155             transition = transition,
156             transitionInfo = info,
157             preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
158             postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks,
159             newFocusedFreeformTask = getNewFocusedFreeformTask(info),
160         )
161         wasPreviousTransitionExitToOverview = info.isExitToRecentsTransition()
162     }
163 
164     override fun onTransitionStarting(transition: IBinder) {}
165 
166     override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
167 
168     override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
169 
170     fun onTaskVanished(taskInfo: RunningTaskInfo) {
171         // At this point the task should have been cleared up due to transition. If it's not yet
172         // cleared up, it might be one of the edge cases where transitions don't give the correct
173         // signal.
174         if (visibleFreeformTaskInfos.containsKey(taskInfo.taskId)) {
175             val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray()
176             postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos)
177             postTransitionFreeformTasks.remove(taskInfo.taskId)
178             ProtoLog.v(
179                 WM_SHELL_DESKTOP_MODE,
180                 "DesktopModeLogger: processing tasks after task vanished %s",
181                 postTransitionFreeformTasks.size(),
182             )
183             identifyLogEventAndUpdateState(
184                 transition = null,
185                 transitionInfo = null,
186                 preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
187                 postTransitionVisibleFreeformTasks = postTransitionFreeformTasks,
188                 newFocusedFreeformTask = null,
189             )
190         }
191     }
192 
193     // Returns null if there was no change in focused task
194     private fun getNewFocusedFreeformTask(info: TransitionInfo): TaskInfo? {
195         val freeformWindowChanges =
196             info.changes
197                 .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID }
198                 .filter { it.requireTaskInfo().isFreeformWindow() }
199         return freeformWindowChanges
200             .findLast { change ->
201                 change.hasFlags(FLAG_MOVED_TO_TOP) || change.mode == TRANSIT_OPEN
202             }
203             ?.taskInfo
204     }
205 
206     private fun getPostTransitionVisibleFreeformTaskInfos(
207         info: TransitionInfo
208     ): SparseArray<TaskInfo> {
209         // device is sleeping, so no task will be visible anymore
210         if (info.type == WindowManager.TRANSIT_SLEEP) {
211             return SparseArray()
212         }
213 
214         // filter changes involving freeform tasks or tasks that were cached in previous state
215         val changesToFreeformWindows =
216             info.changes
217                 .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID }
218                 .filter {
219                     it.requireTaskInfo().isFreeformWindow() ||
220                         visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId)
221                 }
222 
223         val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray()
224         // start off by adding all existing tasks
225         postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos)
226 
227         // the combined set of taskInfos we are interested in this transition change
228         for (change in changesToFreeformWindows) {
229             val taskInfo = change.requireTaskInfo()
230 
231             // check if this task existed as freeform window in previous cached state and it's now
232             // changing window modes
233             if (
234                 visibleFreeformTaskInfos.containsKey(taskInfo.taskId) &&
235                     visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() &&
236                     !taskInfo.isFreeformWindow()
237             ) {
238                 postTransitionFreeformTasks.remove(taskInfo.taskId)
239                 // no need to evaluate new visibility of this task, since it's no longer a freeform
240                 // window
241                 continue
242             }
243 
244             // check if the task is visible after this change, otherwise remove it
245             if (isTaskVisibleAfterChange(change)) {
246                 postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo)
247             } else {
248                 postTransitionFreeformTasks.remove(taskInfo.taskId)
249             }
250         }
251 
252         ProtoLog.v(
253             WM_SHELL_DESKTOP_MODE,
254             "DesktopModeLogger: taskInfo map after processing changes %s",
255             postTransitionFreeformTasks.size(),
256         )
257 
258         return postTransitionFreeformTasks
259     }
260 
261     /**
262      * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this
263      * change is processed
264      */
265     private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean =
266         when {
267             TransitionUtil.isOpeningType(change.mode) -> true
268             TransitionUtil.isClosingType(change.mode) -> false
269             // change mode TRANSIT_CHANGE is only for visible to visible transitions
270             change.mode == WindowManager.TRANSIT_CHANGE -> true
271             else -> false
272         }
273 
274     /**
275      * Log the appropriate log event based on the new state of TasksInfos and previously cached
276      * state and update it
277      */
278     private fun identifyLogEventAndUpdateState(
279         transition: IBinder?,
280         transitionInfo: TransitionInfo?,
281         preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
282         postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
283         newFocusedFreeformTask: TaskInfo?,
284     ) {
285         if (
286             postTransitionVisibleFreeformTasks.isEmpty() &&
287                 preTransitionVisibleFreeformTasks.isNotEmpty() &&
288                 isSessionActive
289         ) {
290             // Sessions is finishing, log task updates followed by an exit event
291             identifyAndLogTaskUpdates(
292                 transition,
293                 transitionInfo,
294                 preTransitionVisibleFreeformTasks,
295                 postTransitionVisibleFreeformTasks,
296                 newFocusedFreeformTask,
297             )
298 
299             desktopModeEventLogger.logSessionExit(getExitReason(transitionInfo))
300             isSessionActive = false
301         } else if (
302             postTransitionVisibleFreeformTasks.isNotEmpty() &&
303                 preTransitionVisibleFreeformTasks.isEmpty() &&
304                 !isSessionActive
305         ) {
306             // Session is starting, log enter event followed by task updates
307             isSessionActive = true
308             desktopModeEventLogger.logSessionEnter(getEnterReason(transitionInfo))
309 
310             identifyAndLogTaskUpdates(
311                 transition,
312                 transitionInfo,
313                 preTransitionVisibleFreeformTasks,
314                 postTransitionVisibleFreeformTasks,
315                 newFocusedFreeformTask,
316             )
317         } else if (isSessionActive) {
318             // Session is neither starting, nor finishing, log task updates if there are any
319             identifyAndLogTaskUpdates(
320                 transition,
321                 transitionInfo,
322                 preTransitionVisibleFreeformTasks,
323                 postTransitionVisibleFreeformTasks,
324                 newFocusedFreeformTask,
325             )
326         }
327 
328         // update the state to the new version
329         visibleFreeformTaskInfos.clear()
330         visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks)
331         focusedFreeformTask = newFocusedFreeformTask
332     }
333 
334     /** Compare the old and new state of taskInfos and identify and log the changes */
335     private fun identifyAndLogTaskUpdates(
336         transition: IBinder?,
337         transitionInfo: TransitionInfo?,
338         preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
339         postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
340         newFocusedFreeformTask: TaskInfo?,
341     ) {
342         postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
343             val focusChangedReason = getFocusChangedReason(taskId, newFocusedFreeformTask)
344             val currentTaskUpdate =
345                 buildTaskUpdateForTask(
346                     taskInfo,
347                     postTransitionVisibleFreeformTasks.size(),
348                     focusChangedReason = focusChangedReason,
349                 )
350             val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId]
351             when {
352                 // new tasks added
353                 previousTaskInfo == null -> {
354                     // The current task is now visible while before it wasn't - this might be the
355                     // result of an unminimize action.
356                     val unminimizeReason = getUnminimizeReason(transition, taskInfo)
357                     desktopModeEventLogger.logTaskAdded(
358                         currentTaskUpdate.copy(unminimizeReason = unminimizeReason)
359                     )
360                     Trace.setCounter(
361                         Trace.TRACE_TAG_WINDOW_MANAGER,
362                         VISIBLE_TASKS_COUNTER_NAME,
363                         postTransitionVisibleFreeformTasks.size().toLong(),
364                     )
365                     SystemProperties.set(
366                         VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
367                         postTransitionVisibleFreeformTasks.size().toString(),
368                     )
369                 }
370                 focusChangedReason != null ->
371                     desktopModeEventLogger.logTaskInfoChanged(currentTaskUpdate)
372                 // old tasks that were resized or repositioned
373                 // TODO(b/347935387): Log changes only once they are stable.
374                 buildTaskUpdateForTask(
375                     previousTaskInfo,
376                     postTransitionVisibleFreeformTasks.size(),
377                     focusChangedReason = focusChangedReason,
378                 ) != currentTaskUpdate ->
379                     desktopModeEventLogger.logTaskInfoChanged(currentTaskUpdate)
380             }
381         }
382 
383         // find old tasks that were removed
384         preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
385             if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) {
386                 // The task is no longer visible, it might have been minimized, get the minimize
387                 // reason (if any)
388                 val minimizeReason = getMinimizeReason(transition, transitionInfo, taskInfo)
389                 val taskUpdate =
390                     buildTaskUpdateForTask(
391                         taskInfo,
392                         postTransitionVisibleFreeformTasks.size(),
393                         minimizeReason,
394                     )
395                 desktopModeEventLogger.logTaskRemoved(taskUpdate)
396                 Trace.setCounter(
397                     Trace.TRACE_TAG_WINDOW_MANAGER,
398                     VISIBLE_TASKS_COUNTER_NAME,
399                     postTransitionVisibleFreeformTasks.size().toLong(),
400                 )
401                 SystemProperties.set(
402                     VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
403                     postTransitionVisibleFreeformTasks.size().toString(),
404                 )
405             }
406         }
407     }
408 
409     private fun getMinimizeReason(
410         transition: IBinder?,
411         transitionInfo: TransitionInfo?,
412         taskInfo: TaskInfo,
413     ): MinimizeReason? {
414         if (transitionInfo?.type == Transitions.TRANSIT_MINIMIZE) {
415             return MinimizeReason.MINIMIZE_BUTTON
416         }
417         val minimizingTask =
418             transition?.let { desktopTasksLimiter.getOrNull()?.getMinimizingTask(transition) }
419         if (minimizingTask?.taskId == taskInfo.taskId) {
420             return minimizingTask.minimizeReason
421         }
422         return null
423     }
424 
425     private fun getUnminimizeReason(transition: IBinder?, taskInfo: TaskInfo): UnminimizeReason? {
426         val unminimizingTask =
427             transition?.let { desktopTasksLimiter.getOrNull()?.getUnminimizingTask(transition) }
428         if (unminimizingTask?.taskId == taskInfo.taskId) {
429             return unminimizingTask.unminimizeReason
430         }
431         return null
432     }
433 
434     private fun getFocusChangedReason(
435         taskId: Int,
436         newFocusedFreeformTask: TaskInfo?,
437     ): FocusReason? {
438         val newFocusedTask = newFocusedFreeformTask ?: return null
439         if (taskId != newFocusedTask.taskId) return null
440         return if (newFocusedTask != focusedFreeformTask) FocusReason.UNKNOWN else null
441     }
442 
443     private fun buildTaskUpdateForTask(
444         taskInfo: TaskInfo,
445         visibleTasks: Int,
446         minimizeReason: MinimizeReason? = null,
447         unminimizeReason: UnminimizeReason? = null,
448         focusChangedReason: FocusReason? = null,
449     ): TaskUpdate {
450         val screenBounds = taskInfo.configuration.windowConfiguration.bounds
451         val positionInParent = taskInfo.positionInParent
452         // We can't both minimize and unminimize the same task in one go.
453         assert(minimizeReason == null || unminimizeReason == null)
454         return TaskUpdate(
455             instanceId = taskInfo.taskId,
456             uid = taskInfo.effectiveUid,
457             taskHeight = screenBounds.height(),
458             taskWidth = screenBounds.width(),
459             taskX = positionInParent.x,
460             taskY = positionInParent.y,
461             visibleTaskCount = visibleTasks,
462             minimizeReason = minimizeReason,
463             unminimizeReason = unminimizeReason,
464             focusReason = focusChangedReason,
465         )
466     }
467 
468     /** Get [EnterReason] for this session enter */
469     private fun getEnterReason(transitionInfo: TransitionInfo?): EnterReason {
470         val enterReason =
471             when {
472                 transitionInfo?.type == WindowManager.TRANSIT_WAKE
473                 // If there is a screen lock, desktop window entry is after dismissing keyguard
474                 ||
475                     (transitionInfo?.type == WindowManager.TRANSIT_TO_BACK &&
476                         wasPreviousTransitionExitByScreenOff) -> EnterReason.SCREEN_ON
477                 transitionInfo?.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP ->
478                     EnterReason.APP_HANDLE_DRAG
479                 transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON ->
480                     EnterReason.APP_HANDLE_MENU_BUTTON
481                 transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW ->
482                     EnterReason.APP_FROM_OVERVIEW
483                 transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT ->
484                     EnterReason.KEYBOARD_SHORTCUT_ENTER
485                 // NOTE: the below condition also applies for EnterReason quickswitch
486                 transitionInfo?.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW
487                 // Enter desktop mode from cancelled recents has no transition. Enter is detected on
488                 // the
489                 // next transition involving freeform windows.
490                 // TODO(b/346564416): Modify logging for cancelled recents once it transition is
491                 //  changed. Also see how to account to time difference between actual enter time
492                 // and
493                 //  time of this log. Also account for the missed session when exit happens just
494                 // after
495                 //  a cancelled recents.
496                 wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW
497                 transitionInfo?.type == WindowManager.TRANSIT_OPEN ->
498                     EnterReason.APP_FREEFORM_INTENT
499                 else -> {
500                     ProtoLog.w(
501                         WM_SHELL_DESKTOP_MODE,
502                         "Unknown enter reason for transition type: %s",
503                         transitionInfo?.type,
504                     )
505                     EnterReason.UNKNOWN_ENTER
506                 }
507             }
508         wasPreviousTransitionExitByScreenOff = false
509         return enterReason
510     }
511 
512     /** Get [ExitReason] for this session exit */
513     private fun getExitReason(transitionInfo: TransitionInfo?): ExitReason =
514         when {
515             transitionInfo?.type == WindowManager.TRANSIT_SLEEP -> {
516                 wasPreviousTransitionExitByScreenOff = true
517                 ExitReason.SCREEN_OFF
518             }
519             // TODO(b/384490301): differentiate back gesture / button exit from clicking the close
520             // button located in the window top corner.
521             transitionInfo?.type == WindowManager.TRANSIT_TO_BACK -> ExitReason.TASK_MOVED_TO_BACK
522             transitionInfo?.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
523             transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT
524             transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON ->
525                 ExitReason.APP_HANDLE_MENU_BUTTON_EXIT
526 
527             transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT ->
528                 ExitReason.KEYBOARD_SHORTCUT_EXIT
529 
530             transitionInfo?.isExitToRecentsTransition() == true ->
531                 ExitReason.RETURN_HOME_OR_OVERVIEW
532             transitionInfo?.type == Transitions.TRANSIT_MINIMIZE -> ExitReason.TASK_MINIMIZED
533             else -> {
534                 ProtoLog.w(
535                     WM_SHELL_DESKTOP_MODE,
536                     "Unknown exit reason for transition type: %s",
537                     transitionInfo?.type,
538                 )
539                 ExitReason.UNKNOWN_EXIT
540             }
541         }
542 
543     /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */
544     @VisibleForTesting
545     fun addTaskInfosToCachedMap(taskInfo: TaskInfo) {
546         visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
547     }
548 
549     /** Sets the focused task - only used for testing. */
550     @VisibleForTesting
551     fun setFocusedTaskForTesting(taskInfo: TaskInfo) {
552         focusedFreeformTask = taskInfo
553     }
554 
555     private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo =
556         this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
557 
558     private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM
559 
560     private fun TransitionInfo.isExitToRecentsTransition(): Boolean =
561         this.type == WindowManager.TRANSIT_TO_FRONT &&
562             this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
563 
564     companion object {
565         @VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks"
566         @VisibleForTesting
567         const val VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY =
568             "debug.tracing." + VISIBLE_TASKS_COUNTER_NAME
569         const val VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE = "0"
570     }
571 }
572