Merge "Add a transition observer to listen to transitions." into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index fb3c35b..04f0f44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -57,6 +57,8 @@
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
+import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -509,6 +511,7 @@
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
@DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
RecentsTransitionHandler recentsTransitionHandler,
MultiInstanceHelper multiInstanceHelper,
@@ -518,7 +521,8 @@
displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
dragAndDropController, transitions, enterDesktopTransitionHandler,
exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController,
+ dragToDesktopTransitionHandler, desktopModeTaskRepository,
+ desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper, mainExecutor);
}
@@ -562,6 +566,22 @@
return new DesktopModeTaskRepository();
}
+ @WMSingleton
+ @Provides
+ static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
+ ShellInit shellInit,
+ Transitions transitions,
+ DesktopModeEventLogger desktopModeEventLogger) {
+ return new DesktopModeLoggerTransitionObserver(
+ shellInit, transitions, desktopModeEventLogger);
+ }
+
+ @WMSingleton
+ @Provides
+ static DesktopModeEventLogger provideDesktopModeEventLogger() {
+ return new DesktopModeEventLogger();
+ }
+
//
// Drag and drop
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
new file mode 100644
index 0000000..a10c7c0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.TaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.SparseArray
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.containsKey
+import androidx.core.util.forEach
+import androidx.core.util.isEmpty
+import androidx.core.util.isNotEmpty
+import androidx.core.util.plus
+import androidx.core.util.putAll
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log
+ * appropriate desktop mode session log events. This observes transitions related to desktop mode
+ * and other transitions that originate both within and outside shell.
+ */
+class DesktopModeLoggerTransitionObserver(
+ shellInit: ShellInit,
+ private val transitions: Transitions,
+ private val desktopModeEventLogger: DesktopModeEventLogger
+) : Transitions.TransitionObserver {
+
+ private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) }
+
+ init {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) {
+ shellInit.addInitCallback(this::onInit, this)
+ }
+ }
+
+ // A sparse array of visible freeform tasks and taskInfos
+ private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray()
+
+ // Caching the taskInfos to handle canceled recents animations, if we identify that the recents
+ // animation was cancelled, we restore these tasks to calculate the post-Transition state
+ private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray()
+
+ // The instanceId for the current logging session
+ private var loggerInstanceId: InstanceId? = null
+
+ private val isSessionActive: Boolean
+ get() = loggerInstanceId != null
+
+ private fun setSessionInactive() {
+ loggerInstanceId = null
+ }
+
+ fun onInit() {
+ transitions.registerObserver(this)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ // this was a new recents animation
+ if (info.isRecentsTransition() && tasksSavedForRecents.isEmpty()) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: Recents animation running, saving tasks for later"
+ )
+ // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled
+ // recents animation
+
+ // when recents animation is running, all freeform tasks are sent TO_BACK temporarily
+ // if the user ends up at home, we need to update the visible freeform tasks
+ // if the user cancels the animation, the subsequent transition is NONE
+ // if the user opens a new task, the subsequent transition is OPEN with flag
+ tasksSavedForRecents.putAll(visibleFreeformTaskInfos)
+ }
+
+ // figure out what the new state of freeform tasks would be post transition
+ var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info)
+
+ // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if
+ // that's the case, we might have accidentally logged a session exit and would need to
+ // revaluate again. Add all the tasks back.
+ // This will start a new desktop mode session.
+ if (
+ info.type == WindowManager.TRANSIT_NONE &&
+ info.flags == 0 &&
+ tasksSavedForRecents.isNotEmpty()
+ ) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: Canceled recents animation, restoring tasks"
+ )
+ // restore saved tasks in the updated set and clear for next use
+ postTransitionVisibleFreeformTasks += tasksSavedForRecents
+ tasksSavedForRecents.clear()
+ }
+
+ // identify if we need to log any changes and update the state of visible freeform tasks
+ identifyLogEventAndUpdateState(
+ transitionInfo = info,
+ preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
+ postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks
+ )
+ }
+
+ override fun onTransitionStarting(transition: IBinder) {}
+
+ override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+
+ override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
+
+ private fun getPostTransitionVisibleFreeformTaskInfos(
+ info: TransitionInfo
+ ): SparseArray<TaskInfo> {
+ // device is sleeping, so no task will be visible anymore
+ if (info.type == WindowManager.TRANSIT_SLEEP) {
+ return SparseArray()
+ }
+
+ // filter changes involving freeform tasks or tasks that were cached in previous state
+ val changesToFreeformWindows =
+ info.changes
+ .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID }
+ .filter {
+ it.requireTaskInfo().isFreeformWindow() ||
+ visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId)
+ }
+
+ val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray()
+ // start off by adding all existing tasks
+ postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos)
+
+ // the combined set of taskInfos we are interested in this transition change
+ for (change in changesToFreeformWindows) {
+ val taskInfo = change.requireTaskInfo()
+
+ // check if this task existed as freeform window in previous cached state and it's now
+ // changing window modes
+ if (
+ visibleFreeformTaskInfos.containsKey(taskInfo.taskId) &&
+ visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() &&
+ !taskInfo.isFreeformWindow()
+ ) {
+ postTransitionFreeformTasks.remove(taskInfo.taskId)
+ // no need to evaluate new visibility of this task, since it's no longer a freeform
+ // window
+ continue
+ }
+
+ // check if the task is visible after this change, otherwise remove it
+ if (isTaskVisibleAfterChange(change)) {
+ postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo)
+ } else {
+ postTransitionFreeformTasks.remove(taskInfo.taskId)
+ }
+ }
+
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: taskInfo map after processing changes %s",
+ postTransitionFreeformTasks.size()
+ )
+
+ return postTransitionFreeformTasks
+ }
+
+ /**
+ * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this
+ * change is processed
+ */
+ private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean =
+ when {
+ TransitionUtil.isOpeningType(change.mode) -> true
+ TransitionUtil.isClosingType(change.mode) -> false
+ // change mode TRANSIT_CHANGE is only for visible to visible transitions
+ change.mode == WindowManager.TRANSIT_CHANGE -> true
+ else -> false
+ }
+
+ /**
+ * Log the appropriate log event based on the new state of TasksInfos and previously cached
+ * state and update it
+ */
+ private fun identifyLogEventAndUpdateState(
+ transitionInfo: TransitionInfo,
+ preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
+ postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
+ ) {
+ if (
+ postTransitionVisibleFreeformTasks.isEmpty() &&
+ preTransitionVisibleFreeformTasks.isNotEmpty() &&
+ isSessionActive
+ ) {
+ // Sessions is finishing, log task updates followed by an exit event
+ identifyAndLogTaskUpdates(
+ loggerInstanceId!!.id,
+ preTransitionVisibleFreeformTasks,
+ postTransitionVisibleFreeformTasks
+ )
+
+ desktopModeEventLogger.logSessionExit(
+ loggerInstanceId!!.id,
+ getExitReason(transitionInfo)
+ )
+
+ setSessionInactive()
+ } else if (
+ postTransitionVisibleFreeformTasks.isNotEmpty() &&
+ preTransitionVisibleFreeformTasks.isEmpty() &&
+ !isSessionActive
+ ) {
+ // Session is starting, log enter event followed by task updates
+ loggerInstanceId = idSequence.newInstanceId()
+ desktopModeEventLogger.logSessionEnter(
+ loggerInstanceId!!.id,
+ getEnterReason(transitionInfo)
+ )
+
+ identifyAndLogTaskUpdates(
+ loggerInstanceId!!.id,
+ preTransitionVisibleFreeformTasks,
+ postTransitionVisibleFreeformTasks
+ )
+ } else if (isSessionActive) {
+ // Session is neither starting, nor finishing, log task updates if there are any
+ identifyAndLogTaskUpdates(
+ loggerInstanceId!!.id,
+ preTransitionVisibleFreeformTasks,
+ postTransitionVisibleFreeformTasks
+ )
+ }
+
+ // update the state to the new version
+ visibleFreeformTaskInfos.clear()
+ visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks)
+ }
+
+ // TODO(b/326231724) - Add logging around taskInfoChanges Updates
+ /** Compare the old and new state of taskInfos and identify and log the changes */
+ private fun identifyAndLogTaskUpdates(
+ sessionId: Int,
+ preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
+ postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
+ ) {
+ // find new tasks that were added
+ postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
+ if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) {
+ desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo))
+ }
+ }
+
+ // find old tasks that were removed
+ preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
+ if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) {
+ desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo))
+ }
+ }
+ }
+
+ // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo
+ private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate {
+ val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId)
+ // add task x, y if available
+ taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) }
+
+ return taskUpdate
+ }
+
+ /** Get [EnterReason] for this session enter */
+ private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason {
+ // TODO(b/326231756) - Add support for missing enter reasons
+ return when (transitionInfo.type) {
+ WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON
+ Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG
+ Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON
+ WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT
+ else -> EnterReason.UNKNOWN_ENTER
+ }
+ }
+
+ /** Get [ExitReason] for this session exit */
+ private fun getExitReason(transitionInfo: TransitionInfo): ExitReason {
+ // TODO(b/326231756) - Add support for missing exit reasons
+ return when {
+ transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF
+ transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
+ transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT
+ transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW
+ else -> ExitReason.UNKNOWN_EXIT
+ }
+ }
+
+ /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */
+ @VisibleForTesting
+ fun addTaskInfosToCachedMap(taskInfo: TaskInfo) {
+ visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
+ }
+
+ @VisibleForTesting fun getLoggerSessionId(): Int? = loggerInstanceId?.id
+
+ @VisibleForTesting
+ fun setLoggerSessionId(id: Int) {
+ loggerInstanceId = InstanceId.fakeInstanceId(id)
+ }
+
+ private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo {
+ return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
+ }
+
+ private fun TaskInfo.isFreeformWindow(): Boolean {
+ return this.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+
+ private fun TransitionInfo.isRecentsTransition(): Boolean {
+ return this.type == WindowManager.TRANSIT_TO_FRONT &&
+ this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 95237c3..dd8c1a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -102,6 +102,7 @@
ToggleResizeDesktopTaskTransitionHandler,
private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
private val desktopModeTaskRepository: DesktopModeTaskRepository,
+ private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
private val launchAdjacentController: LaunchAdjacentController,
private val recentsTransitionHandler: RecentsTransitionHandler,
private val multiInstanceHelper: MultiInstanceHelper,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
new file mode 100644
index 0000000..65117f7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS
+import android.view.WindowManager.TRANSIT_NONE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_SLEEP
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.view.WindowManager.TRANSIT_WAKE
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.TransitionInfoBuilder
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.times
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopModeLoggerTransitionObserverTest {
+
+ @JvmField
+ @Rule
+ val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
+ .mockStatic(DesktopModeEventLogger::class.java)
+ .mockStatic(DesktopModeStatus::class.java).build()!!
+
+ @Mock
+ lateinit var testExecutor: ShellExecutor
+ @Mock
+ private lateinit var mockShellInit: ShellInit
+ @Mock
+ private lateinit var transitions: Transitions
+
+ private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
+ private lateinit var shellInit: ShellInit
+ private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+
+ @Before
+ fun setup() {
+ Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true)
+ shellInit = Mockito.spy(ShellInit(testExecutor))
+ desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
+
+ transitionObserver = DesktopModeLoggerTransitionObserver(
+ mockShellInit, transitions, desktopModeEventLogger)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ val initRunnableCaptor = ArgumentCaptor.forClass(
+ Runnable::class.java)
+ verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(),
+ same(transitionObserver))
+ initRunnableCaptor.value.run()
+ } else {
+ transitionObserver.onInit()
+ }
+ }
+
+ @Test
+ fun testRegistersObserverAtInit() {
+ verify(transitions)
+ .registerObserver(same(
+ transitionObserver))
+ }
+
+ @Test
+ fun taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
+ }
+
+ @Test
+ fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_FREEFORM_INTENT))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ // task change is finalised when drag ends
+ val transitionInfo = TransitionInfoBuilder(
+ Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_HANDLE_DRAG))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
+ .addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() {
+ val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+ taskInfo.isVisibleRequested = true
+ val change = createChange(TRANSIT_CHANGE, taskInfo)
+ val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
+ .addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskToFront_screenWake_logSessionStartedAndTaskAdded() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0)
+ .addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.SCREEN_ON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.SCREEN_OFF))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE)
+ .addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.DRAG_TO_EXIT))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task closing
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.TASK_FINISHED))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+ val sessionId = 1
+ // add a freeform task to an existing session
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition sent freeform window to back
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo1 =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change)
+ .build()
+ callOnTransitionReady(transitionInfo1)
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+
+ val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+ callOnTransitionReady(transitionInfo2)
+
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+ val sessionId = 1
+ // add an existing freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+ val sessionId = 1
+ // add two existing freeform tasks
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
+ }
+
+ /**
+ * Simulate calling the onTransitionReady() method
+ */
+ private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+ val transition = mock(IBinder::class.java)
+ val startT = mock(
+ SurfaceControl.Transaction::class.java)
+ val finishT = mock(
+ SurfaceControl.Transaction::class.java)
+
+ transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+ }
+
+ companion object {
+ fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
+ val taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = taskId
+ taskInfo.configuration.windowConfiguration.windowingMode = windowMode
+
+ return taskInfo
+ }
+
+ fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
+ val change = Change(
+ WindowContainerToken(mock(
+ IWindowContainerToken::class.java)),
+ mock(SurfaceControl::class.java))
+ change.mode = mode
+ change.taskInfo = taskInfo
+ return change
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 0136751..5df9dd3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -87,6 +87,7 @@
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
+import org.mockito.kotlin.times
import org.mockito.Mockito.`when` as whenever
import org.mockito.quality.Strictness
@@ -113,6 +114,7 @@
@Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@Mock lateinit var dragAndDropController: DragAndDropController
@Mock lateinit var multiInstanceHelper: MultiInstanceHelper
+ @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var controller: DesktopTasksController
@@ -163,6 +165,7 @@
mToggleResizeDesktopTaskTransitionHandler,
dragToDesktopTransitionHandler,
desktopModeTaskRepository,
+ desktopModeLoggerTransitionObserver,
launchAdjacentController,
recentsTransitionHandler,
multiInstanceHelper,