diff options
| author | 2024-03-12 10:22:16 +0000 | |
|---|---|---|
| committer | 2024-03-12 10:22:16 +0000 | |
| commit | 92b2dcc786edcc77fa3ef77f473754e8abdc77b7 (patch) | |
| tree | 837b7029afdc5f4d42d8f7fbb957629f114ff96e | |
| parent | 07f20d3e6833b3f7615b55faef03329bf6e32ad4 (diff) | |
| parent | 0c8d33411944563563d7dc0864f0526432ef3ab6 (diff) | |
Merge "Add a transition observer to listen to transitions." into main
5 files changed, 732 insertions, 1 deletions
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 fb3c35b6a1e3..04f0f44d2876 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.ShellBackgroundThread; 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 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, @@ -518,7 +521,8 @@ public abstract class WMShellModule { displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController, + dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor); } @@ -562,6 +566,22 @@ public abstract class WMShellModule { 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 000000000000..a10c7c093c60 --- /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 95237c38f309..dd8c1a0f2e02 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 @@ class DesktopTasksController( 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 000000000000..65117f7e9eea --- /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 0136751d8c9a..5df9dd38a75d 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.any 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 @@ class DesktopTasksControllerTest : ShellTestCase() { @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 @@ class DesktopTasksControllerTest : ShellTestCase() { mToggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, |