diff options
6 files changed, 382 insertions, 67 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt new file mode 100644 index 000000000000..fc5cf7d75bdf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 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.systemui.mediaprojection.taskswitcher.domain.interactor + +import android.app.TaskInfo +import android.content.Intent +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository +import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** Interactor with logic related to task switching in the context of media projection. */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class TaskSwitchInteractor +@Inject +constructor( + mediaProjectionRepository: MediaProjectionRepository, + private val tasksRepository: TasksRepository, +) { + + /** + * Emits a stream of changes to the state of task switching, in the context of media projection. + */ + val taskSwitchChanges: Flow<TaskSwitchState> = + mediaProjectionRepository.mediaProjectionState.flatMapLatest { projectionState -> + Log.d(TAG, "MediaProjectionState -> $projectionState") + when (projectionState) { + is MediaProjectionState.SingleTask -> { + val projectedTask = projectionState.task + tasksRepository.foregroundTask.map { foregroundTask -> + if (hasForegroundTaskSwitched(projectedTask, foregroundTask)) { + TaskSwitchState.TaskSwitched(projectedTask, foregroundTask) + } else { + TaskSwitchState.TaskUnchanged + } + } + } + is MediaProjectionState.EntireScreen, + is MediaProjectionState.NotProjecting -> { + flowOf(TaskSwitchState.NotProjectingTask) + } + } + } + + /** + * Returns whether tasks have been switched. + * + * Always returns `false` when launcher is in the foreground. The reason is that when going to + * recents to switch apps, launcher becomes the new foreground task, and we don't want to show + * the notification then. + */ + private fun hasForegroundTaskSwitched(projectedTask: TaskInfo, foregroundTask: TaskInfo) = + projectedTask.taskId != foregroundTask.taskId && !foregroundTask.isLauncher + + private val TaskInfo.isLauncher + get() = + baseIntent.hasCategory(Intent.CATEGORY_HOME) && baseIntent.action == Intent.ACTION_MAIN + + companion object { + private const val TAG = "TaskSwitchInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt new file mode 100644 index 000000000000..cd1258ed6aa8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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.systemui.mediaprojection.taskswitcher.domain.model + +import android.app.TaskInfo + +/** Represents tha state of task switching in the context of single task media projection. */ +sealed interface TaskSwitchState { + /** Currently no task is being projected. */ + object NotProjectingTask : TaskSwitchState + /** The foreground task is the same as the task that is currently being projected. */ + object TaskUnchanged : TaskSwitchState + /** The foreground task is a different one to the task it currently being projected. */ + data class TaskSwitched(val projectedTask: TaskInfo, val foregroundTask: TaskInfo) : + TaskSwitchState +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt index efd21603abad..83932b0a6133 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt @@ -16,67 +16,41 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository -import android.app.ActivityManager.RunningTaskInfo -import android.app.ActivityTaskManager -import android.app.TaskStackListener import android.os.Binder import android.testing.AndroidTestingRunner -import android.window.IWindowContainerToken -import android.window.WindowContainerToken import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @SmallTest class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { - @Mock private lateinit var activityTaskManager: ActivityTaskManager + private val fakeActivityTaskManager = FakeActivityTaskManager() - private val dispatcher = StandardTestDispatcher() + private val dispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(dispatcher) - private lateinit var repo: ActivityTaskManagerTasksRepository - private lateinit var taskStackListener: TaskStackListener - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(activityTaskManager.registerTaskStackListener(any())).thenAnswer { - taskStackListener = it.arguments[0] as TaskStackListener - return@thenAnswer Unit - } - repo = - ActivityTaskManagerTasksRepository( - activityTaskManager, - applicationScope = testScope.backgroundScope, - backgroundDispatcher = dispatcher - ) - } + private val repo = + ActivityTaskManagerTasksRepository( + activityTaskManager = fakeActivityTaskManager.activityTaskManager, + applicationScope = testScope.backgroundScope, + backgroundDispatcher = dispatcher + ) @Test fun findRunningTaskFromWindowContainerToken_noMatch_returnsNull() { - whenever(activityTaskManager.getTasks(Integer.MAX_VALUE)) - .thenReturn( - listOf( - createTaskInfo(newTaskId = 1, windowContainerToken = createToken()), - createTaskInfo(newTaskId = 2, windowContainerToken = createToken()) - ) - ) + fakeActivityTaskManager.addRunningTasks(createTask(taskId = 1), createTask(taskId = 2)) testScope.runTest { val matchingTask = @@ -89,15 +63,12 @@ class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { @Test fun findRunningTaskFromWindowContainerToken_matchingToken_returnsTaskInfo() { val expectedToken = createToken() - val expectedTask = createTaskInfo(newTaskId = 1, windowContainerToken = expectedToken) + val expectedTask = createTask(taskId = 1, token = expectedToken) - whenever(activityTaskManager.getTasks(Integer.MAX_VALUE)) - .thenReturn( - listOf( - createTaskInfo(newTaskId = 2, windowContainerToken = createToken()), - expectedTask - ) - ) + fakeActivityTaskManager.addRunningTasks( + createTask(taskId = 2), + expectedTask, + ) testScope.runTest { val actualTask = @@ -113,15 +84,14 @@ class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { fun foregroundTask_returnsStreamOfTasksMovedToFront() = testScope.runTest { val foregroundTask by collectLastValue(repo.foregroundTask) - runCurrent() - taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 1)) + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) assertThat(foregroundTask?.taskId).isEqualTo(1) - taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 2)) + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 2)) assertThat(foregroundTask?.taskId).isEqualTo(2) - taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 3)) + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 3)) assertThat(foregroundTask?.taskId).isEqualTo(3) } @@ -129,26 +99,10 @@ class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { fun foregroundTask_lastValueIsCached() = testScope.runTest { val foregroundTaskA by collectLastValue(repo.foregroundTask) - runCurrent() - taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 1)) + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) assertThat(foregroundTaskA?.taskId).isEqualTo(1) val foregroundTaskB by collectLastValue(repo.foregroundTask) assertThat(foregroundTaskB?.taskId).isEqualTo(1) } - - private fun createToken(): WindowContainerToken { - val realToken = object : IWindowContainerToken.Stub() {} - return WindowContainerToken(realToken) - } - - private fun createTaskInfo( - windowContainerToken: WindowContainerToken = createToken(), - newTaskId: Int, - ): RunningTaskInfo { - return RunningTaskInfo().apply { - token = windowContainerToken - taskId = newTaskId - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt new file mode 100644 index 000000000000..1c4870bc32b1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 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.systemui.mediaprojection.taskswitcher.data.repository + +import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager +import android.app.TaskStackListener +import android.content.Intent +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +class FakeActivityTaskManager { + + private val runningTasks = mutableListOf<RunningTaskInfo>() + private val taskTaskListeners = mutableListOf<TaskStackListener>() + + val activityTaskManager = mock<ActivityTaskManager>() + + init { + whenever(activityTaskManager.registerTaskStackListener(any())).thenAnswer { + taskTaskListeners += it.arguments[0] as TaskStackListener + return@thenAnswer Unit + } + whenever(activityTaskManager.unregisterTaskStackListener(any())).thenAnswer { + taskTaskListeners -= it.arguments[0] as TaskStackListener + return@thenAnswer Unit + } + whenever(activityTaskManager.getTasks(any())).thenAnswer { + val maxNumTasks = it.arguments[0] as Int + return@thenAnswer runningTasks.take(maxNumTasks) + } + } + + fun moveTaskToForeground(task: RunningTaskInfo) { + taskTaskListeners.forEach { it.onTaskMovedToFront(task) } + } + + fun addRunningTasks(vararg tasks: RunningTaskInfo) { + runningTasks += tasks + } + + companion object { + + fun createTask( + taskId: Int, + token: WindowContainerToken = createToken(), + baseIntent: Intent = Intent() + ) = + RunningTaskInfo().apply { + this.taskId = taskId + this.token = token + this.baseIntent = baseIntent + } + + fun createToken(): WindowContainerToken { + val realToken = object : IWindowContainerToken.Stub() {} + return WindowContainerToken(realToken) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt new file mode 100644 index 000000000000..c59fd60cca9b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 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.systemui.mediaprojection.taskswitcher.data.repository + +import android.app.TaskInfo +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeMediaProjectionRepository : MediaProjectionRepository { + + private val state = MutableStateFlow<MediaProjectionState>(MediaProjectionState.NotProjecting) + + fun switchProjectedTask(newTask: TaskInfo) { + state.value = MediaProjectionState.SingleTask(newTask) + } + + override val mediaProjectionState: Flow<MediaProjectionState> = state.asStateFlow() + + fun projectEntireScreen() { + state.value = MediaProjectionState.EntireScreen + } + + fun stopProjecting() { + state.value = MediaProjectionState.NotProjecting + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt new file mode 100644 index 000000000000..112950b860e8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 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.systemui.mediaprojection.taskswitcher.domain.interactor + +import android.content.Intent +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TaskSwitchInteractorTest : SysuiTestCase() { + + private val dispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(dispatcher) + + private val fakeActivityTaskManager = FakeActivityTaskManager() + private val mediaRepo = FakeMediaProjectionRepository() + private val tasksRepo = + ActivityTaskManagerTasksRepository( + activityTaskManager = fakeActivityTaskManager.activityTaskManager, + applicationScope = testScope.backgroundScope, + backgroundDispatcher = dispatcher + ) + + private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) + + @Test + fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() = + testScope.runTest { + mediaRepo.stopProjecting() + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask) + } + + @Test + fun taskSwitchChanges_projectingScreen_foregroundTaskChange_emitsNotProjectingTask() = + testScope.runTest { + mediaRepo.projectEntireScreen() + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskDifferent_emitsTaskChanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) + mediaRepo.switchProjectedTask(projectedTask) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(taskSwitchState) + .isEqualTo( + TaskSwitchState.TaskSwitched( + projectedTask = projectedTask, + foregroundTask = foregroundTask + ) + ) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskLauncher_emitsTaskUnchanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1, baseIntent = LAUNCHER_INTENT) + mediaRepo.switchProjectedTask(projectedTask) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) + } + + @Test + fun taskSwitchChanges_projectingTask_foregroundTaskSame_emitsTaskUnchanged() = + testScope.runTest { + val projectedTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 0) + mediaRepo.switchProjectedTask(projectedTask) + val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) + } + + companion object { + private val LAUNCHER_INTENT: Intent = + Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + } +} |