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.util.SparseArray 26 import android.view.SurfaceControl 27 import android.view.WindowManager 28 import android.window.TransitionInfo 29 import androidx.annotation.VisibleForTesting 30 import androidx.core.util.containsKey 31 import androidx.core.util.forEach 32 import androidx.core.util.isEmpty 33 import androidx.core.util.isNotEmpty 34 import androidx.core.util.plus 35 import androidx.core.util.putAll 36 import com.android.internal.logging.InstanceId 37 import com.android.internal.logging.InstanceIdSequence 38 import com.android.internal.protolog.common.ProtoLog 39 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason 40 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason 41 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate 42 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW 43 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON 44 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT 45 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON 46 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT 47 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG 48 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 49 import com.android.wm.shell.shared.DesktopModeStatus 50 import com.android.wm.shell.shared.TransitionUtil 51 import com.android.wm.shell.sysui.ShellInit 52 import com.android.wm.shell.transition.Transitions 53 import com.android.wm.shell.util.KtProtoLog 54 55 /** 56 * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log 57 * appropriate desktop mode session log events. This observes transitions related to desktop mode 58 * and other transitions that originate both within and outside shell. 59 */ 60 class DesktopModeLoggerTransitionObserver( 61 context: Context, 62 shellInit: ShellInit, 63 private val transitions: Transitions, 64 private val desktopModeEventLogger: DesktopModeEventLogger 65 ) : Transitions.TransitionObserver { 66 67 private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) } 68 69 init { 70 if ( 71 Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.canEnterDesktopMode(context) 72 ) { 73 shellInit.addInitCallback(this::onInit, this) 74 } 75 } 76 77 // A sparse array of visible freeform tasks and taskInfos 78 private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray() 79 80 // Caching the taskInfos to handle canceled recents animations, if we identify that the recents 81 // animation was cancelled, we restore these tasks to calculate the post-Transition state 82 private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray() 83 84 // Caching whether the previous transition was exit to overview. 85 private var wasPreviousTransitionExitToOverview: Boolean = false 86 87 // The instanceId for the current logging session 88 private var loggerInstanceId: InstanceId? = null 89 90 private val isSessionActive: Boolean 91 get() = loggerInstanceId != null 92 93 private fun setSessionInactive() { 94 loggerInstanceId = null 95 } 96 97 fun onInit() { 98 transitions.registerObserver(this) 99 } 100 101 override fun onTransitionReady( 102 transition: IBinder, 103 info: TransitionInfo, 104 startTransaction: SurfaceControl.Transaction, 105 finishTransaction: SurfaceControl.Transaction 106 ) { 107 // this was a new recents animation 108 if (info.isExitToRecentsTransition() && tasksSavedForRecents.isEmpty()) { 109 KtProtoLog.v( 110 WM_SHELL_DESKTOP_MODE, 111 "DesktopModeLogger: Recents animation running, saving tasks for later" 112 ) 113 // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled 114 // recents animation 115 116 // when recents animation is running, all freeform tasks are sent TO_BACK temporarily 117 // if the user ends up at home, we need to update the visible freeform tasks 118 // if the user cancels the animation, the subsequent transition is NONE 119 // if the user opens a new task, the subsequent transition is OPEN with flag 120 tasksSavedForRecents.putAll(visibleFreeformTaskInfos) 121 } 122 123 // figure out what the new state of freeform tasks would be post transition 124 var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info) 125 126 // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if 127 // that's the case, we might have accidentally logged a session exit and would need to 128 // revaluate again. Add all the tasks back. 129 // This will start a new desktop mode session. 130 if ( 131 info.type == WindowManager.TRANSIT_NONE && 132 info.flags == 0 && 133 tasksSavedForRecents.isNotEmpty() 134 ) { 135 KtProtoLog.v( 136 WM_SHELL_DESKTOP_MODE, 137 "DesktopModeLogger: Canceled recents animation, restoring tasks" 138 ) 139 // restore saved tasks in the updated set and clear for next use 140 postTransitionVisibleFreeformTasks += tasksSavedForRecents 141 tasksSavedForRecents.clear() 142 } 143 144 // identify if we need to log any changes and update the state of visible freeform tasks 145 identifyLogEventAndUpdateState( 146 transitionInfo = info, 147 preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos, 148 postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks 149 ) 150 wasPreviousTransitionExitToOverview = info.isExitToRecentsTransition() 151 } 152 153 override fun onTransitionStarting(transition: IBinder) {} 154 155 override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} 156 157 override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {} 158 159 private fun getPostTransitionVisibleFreeformTaskInfos( 160 info: TransitionInfo 161 ): SparseArray<TaskInfo> { 162 // device is sleeping, so no task will be visible anymore 163 if (info.type == WindowManager.TRANSIT_SLEEP) { 164 return SparseArray() 165 } 166 167 // filter changes involving freeform tasks or tasks that were cached in previous state 168 val changesToFreeformWindows = 169 info.changes 170 .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID } 171 .filter { 172 it.requireTaskInfo().isFreeformWindow() || 173 visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId) 174 } 175 176 val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray() 177 // start off by adding all existing tasks 178 postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos) 179 180 // the combined set of taskInfos we are interested in this transition change 181 for (change in changesToFreeformWindows) { 182 val taskInfo = change.requireTaskInfo() 183 184 // check if this task existed as freeform window in previous cached state and it's now 185 // changing window modes 186 if ( 187 visibleFreeformTaskInfos.containsKey(taskInfo.taskId) && 188 visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() && 189 !taskInfo.isFreeformWindow() 190 ) { 191 postTransitionFreeformTasks.remove(taskInfo.taskId) 192 // no need to evaluate new visibility of this task, since it's no longer a freeform 193 // window 194 continue 195 } 196 197 // check if the task is visible after this change, otherwise remove it 198 if (isTaskVisibleAfterChange(change)) { 199 postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo) 200 } else { 201 postTransitionFreeformTasks.remove(taskInfo.taskId) 202 } 203 } 204 205 KtProtoLog.v( 206 WM_SHELL_DESKTOP_MODE, 207 "DesktopModeLogger: taskInfo map after processing changes %s", 208 postTransitionFreeformTasks.size() 209 ) 210 211 return postTransitionFreeformTasks 212 } 213 214 /** 215 * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this 216 * change is processed 217 */ 218 private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean = 219 when { 220 TransitionUtil.isOpeningType(change.mode) -> true 221 TransitionUtil.isClosingType(change.mode) -> false 222 // change mode TRANSIT_CHANGE is only for visible to visible transitions 223 change.mode == WindowManager.TRANSIT_CHANGE -> true 224 else -> false 225 } 226 227 /** 228 * Log the appropriate log event based on the new state of TasksInfos and previously cached 229 * state and update it 230 */ 231 private fun identifyLogEventAndUpdateState( 232 transitionInfo: TransitionInfo, 233 preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, 234 postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> 235 ) { 236 if ( 237 postTransitionVisibleFreeformTasks.isEmpty() && 238 preTransitionVisibleFreeformTasks.isNotEmpty() && 239 isSessionActive 240 ) { 241 // Sessions is finishing, log task updates followed by an exit event 242 identifyAndLogTaskUpdates( 243 loggerInstanceId!!.id, 244 preTransitionVisibleFreeformTasks, 245 postTransitionVisibleFreeformTasks 246 ) 247 248 desktopModeEventLogger.logSessionExit( 249 loggerInstanceId!!.id, 250 getExitReason(transitionInfo) 251 ) 252 253 setSessionInactive() 254 } else if ( 255 postTransitionVisibleFreeformTasks.isNotEmpty() && 256 preTransitionVisibleFreeformTasks.isEmpty() && 257 !isSessionActive 258 ) { 259 // Session is starting, log enter event followed by task updates 260 loggerInstanceId = idSequence.newInstanceId() 261 desktopModeEventLogger.logSessionEnter( 262 loggerInstanceId!!.id, 263 getEnterReason(transitionInfo) 264 ) 265 266 identifyAndLogTaskUpdates( 267 loggerInstanceId!!.id, 268 preTransitionVisibleFreeformTasks, 269 postTransitionVisibleFreeformTasks 270 ) 271 } else if (isSessionActive) { 272 // Session is neither starting, nor finishing, log task updates if there are any 273 identifyAndLogTaskUpdates( 274 loggerInstanceId!!.id, 275 preTransitionVisibleFreeformTasks, 276 postTransitionVisibleFreeformTasks 277 ) 278 } 279 280 // update the state to the new version 281 visibleFreeformTaskInfos.clear() 282 visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks) 283 } 284 285 // TODO(b/326231724) - Add logging around taskInfoChanges Updates 286 /** Compare the old and new state of taskInfos and identify and log the changes */ 287 private fun identifyAndLogTaskUpdates( 288 sessionId: Int, 289 preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, 290 postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> 291 ) { 292 // find new tasks that were added 293 postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> 294 if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) { 295 desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo)) 296 } 297 } 298 299 // find old tasks that were removed 300 preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> 301 if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { 302 desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo)) 303 } 304 } 305 } 306 307 // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo 308 private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate { 309 val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId) 310 // add task x, y if available 311 taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) } 312 313 return taskUpdate 314 } 315 316 /** Get [EnterReason] for this session enter */ 317 private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason = 318 when { 319 transitionInfo.type == WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON 320 transitionInfo.type == Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> 321 EnterReason.APP_HANDLE_DRAG 322 transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON -> 323 EnterReason.APP_HANDLE_MENU_BUTTON 324 transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW -> 325 EnterReason.APP_FROM_OVERVIEW 326 transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT -> 327 EnterReason.KEYBOARD_SHORTCUT_ENTER 328 // NOTE: the below condition also applies for EnterReason quickswitch 329 transitionInfo.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW 330 // Enter desktop mode from cancelled recents has no transition. Enter is detected on the 331 // next transition involving freeform windows. 332 // TODO(b/346564416): Modify logging for cancelled recents once it transition is 333 // changed. Also see how to account to time difference between actual enter time and 334 // time of this log. Also account for the missed session when exit happens just after 335 // a cancelled recents. 336 wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW 337 transitionInfo.type == WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT 338 else -> { 339 ProtoLog.w( 340 WM_SHELL_DESKTOP_MODE, 341 "Unknown enter reason for transition type ${transitionInfo.type}", 342 transitionInfo.type 343 ) 344 EnterReason.UNKNOWN_ENTER 345 } 346 } 347 348 /** Get [ExitReason] for this session exit */ 349 private fun getExitReason(transitionInfo: TransitionInfo): ExitReason = 350 when { 351 transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF 352 transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED 353 transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT 354 transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -> 355 ExitReason.APP_HANDLE_MENU_BUTTON_EXIT 356 transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -> 357 ExitReason.KEYBOARD_SHORTCUT_EXIT 358 transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW 359 else -> { 360 ProtoLog.w( 361 WM_SHELL_DESKTOP_MODE, 362 "Unknown exit reason for transition type ${transitionInfo.type}", 363 transitionInfo.type 364 ) 365 ExitReason.UNKNOWN_EXIT 366 } 367 } 368 369 /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */ 370 @VisibleForTesting 371 fun addTaskInfosToCachedMap(taskInfo: TaskInfo) { 372 visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo) 373 } 374 375 @VisibleForTesting fun getLoggerSessionId(): Int? = loggerInstanceId?.id 376 377 @VisibleForTesting 378 fun setLoggerSessionId(id: Int) { 379 loggerInstanceId = InstanceId.fakeInstanceId(id) 380 } 381 382 private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo { 383 return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") 384 } 385 386 private fun TaskInfo.isFreeformWindow(): Boolean { 387 return this.windowingMode == WINDOWING_MODE_FREEFORM 388 } 389 390 private fun TransitionInfo.isExitToRecentsTransition(): Boolean { 391 return this.type == WindowManager.TRANSIT_TO_FRONT && 392 this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS 393 } 394 } 395