diff options
6 files changed, 355 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt new file mode 100644 index 000000000000..9938f11e5d4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt @@ -0,0 +1,26 @@ +/* + * 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.model + +import android.app.TaskInfo + +/** Represents the state of media projection. */ +sealed interface MediaProjectionState { + object NotProjecting : MediaProjectionState + object EntireScreen : MediaProjectionState + data class SingleTask(val task: TaskInfo) : MediaProjectionState +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt new file mode 100644 index 000000000000..492d482459d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt @@ -0,0 +1,75 @@ +/* + * 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.os.IBinder +import android.util.Log +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.withContext + +/** Implementation of [TasksRepository] that uses [ActivityTaskManager] as the data source. */ +@SysUISingleton +class ActivityTaskManagerTasksRepository +@Inject +constructor( + private val activityTaskManager: ActivityTaskManager, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : TasksRepository { + + override suspend fun findRunningTaskFromWindowContainerToken( + windowContainerToken: IBinder + ): RunningTaskInfo? = + getRunningTasks().firstOrNull { taskInfo -> + taskInfo.token.asBinder() == windowContainerToken + } + + private suspend fun getRunningTasks(): List<RunningTaskInfo> = + withContext(backgroundDispatcher) { activityTaskManager.getTasks(Integer.MAX_VALUE) } + + override val foregroundTask: Flow<RunningTaskInfo> = + conflatedCallbackFlow { + val listener = + object : TaskStackListener() { + override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) { + Log.d(TAG, "onTaskMovedToFront: $taskInfo") + trySendWithFailureLogging(taskInfo, TAG) + } + } + activityTaskManager.registerTaskStackListener(listener) + awaitClose { activityTaskManager.unregisterTaskStackListener(listener) } + } + .shareIn(applicationScope, SharingStarted.Lazily, replay = 1) + + companion object { + private const val TAG = "TasksRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt new file mode 100644 index 000000000000..5bec6925babe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt @@ -0,0 +1,27 @@ +/* + * 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 com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import kotlinx.coroutines.flow.Flow + +/** Represents a repository to retrieve and change data related to media projection. */ +interface MediaProjectionRepository { + + /** Represents the current [MediaProjectionState]. */ + val mediaProjectionState: Flow<MediaProjectionState> +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt new file mode 100644 index 000000000000..d2fecbbf4b9e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt @@ -0,0 +1,32 @@ +/* + * 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 com.android.systemui.dagger.SysUISingleton +import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +/** + * No-op implementation of [MediaProjectionRepository] that does nothing. Currently used as a + * placeholder, while the real implementation is not completed. + */ +@SysUISingleton +class NoOpMediaProjectionRepository : MediaProjectionRepository { + + override val mediaProjectionState: Flow<MediaProjectionState> = emptyFlow() +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt new file mode 100644 index 000000000000..6a535e4ecc50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.os.IBinder +import kotlinx.coroutines.flow.Flow + +/** Repository responsible for retrieving data related to running tasks. */ +interface TasksRepository { + + /** + * Tries to find a [RunningTaskInfo] with a matching window container token. Returns `null` when + * no matching task was found. + */ + suspend fun findRunningTaskFromWindowContainerToken( + windowContainerToken: IBinder + ): RunningTaskInfo? + + /** + * Emits a stream of [RunningTaskInfo] that have been moved to the foreground. + * + * Note: when subscribing for the first time, it will not immediately emit the current + * foreground task. Only after a change in foreground task has occurred. + */ + val foregroundTask: Flow<RunningTaskInfo> +} 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 new file mode 100644 index 000000000000..efd21603abad --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt @@ -0,0 +1,154 @@ +/* + * 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.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.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.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 dispatcher = StandardTestDispatcher() + 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 + ) + } + + @Test + fun findRunningTaskFromWindowContainerToken_noMatch_returnsNull() { + whenever(activityTaskManager.getTasks(Integer.MAX_VALUE)) + .thenReturn( + listOf( + createTaskInfo(newTaskId = 1, windowContainerToken = createToken()), + createTaskInfo(newTaskId = 2, windowContainerToken = createToken()) + ) + ) + + testScope.runTest { + val matchingTask = + repo.findRunningTaskFromWindowContainerToken(windowContainerToken = Binder()) + + assertThat(matchingTask).isNull() + } + } + + @Test + fun findRunningTaskFromWindowContainerToken_matchingToken_returnsTaskInfo() { + val expectedToken = createToken() + val expectedTask = createTaskInfo(newTaskId = 1, windowContainerToken = expectedToken) + + whenever(activityTaskManager.getTasks(Integer.MAX_VALUE)) + .thenReturn( + listOf( + createTaskInfo(newTaskId = 2, windowContainerToken = createToken()), + expectedTask + ) + ) + + testScope.runTest { + val actualTask = + repo.findRunningTaskFromWindowContainerToken( + windowContainerToken = expectedToken.asBinder() + ) + + assertThat(actualTask).isEqualTo(expectedTask) + } + } + + @Test + fun foregroundTask_returnsStreamOfTasksMovedToFront() = + testScope.runTest { + val foregroundTask by collectLastValue(repo.foregroundTask) + runCurrent() + + taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 1)) + assertThat(foregroundTask?.taskId).isEqualTo(1) + + taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 2)) + assertThat(foregroundTask?.taskId).isEqualTo(2) + + taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 3)) + assertThat(foregroundTask?.taskId).isEqualTo(3) + } + + @Test + fun foregroundTask_lastValueIsCached() = + testScope.runTest { + val foregroundTaskA by collectLastValue(repo.foregroundTask) + runCurrent() + taskStackListener.onTaskMovedToFront(createTaskInfo(newTaskId = 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 + } + } +} |