Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6f22f3af authored by Christian Göllner's avatar Christian Göllner Committed by Chris Göllner
Browse files

Partial Screen Sharing: Task Switcher - Domain Layer implementation

Implements the domain layer:
- Defines model/state
- Implements the interactor that is responsible for knowing whether a
  task switch has happened, in the context of task projection
- Implements fake repositories to be used in unit tests

Bug: 286201261
Test: TaskSwitchInteractorTest.kt
Change-Id: I46ff1214c97a52e63893bc728c2fa191b08b7c6e
parent 92f65330
Loading
Loading
Loading
Loading
+85 −0
Original line number Diff line number Diff line
/*
 * 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"
    }
}
+30 −0
Original line number Diff line number Diff line
/*
 * 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
}
+21 −67
Original line number Diff line number Diff line
@@ -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 =
    private val repo =
        ActivityTaskManagerTasksRepository(
                activityTaskManager,
            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,14 +63,11 @@ 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 {
@@ -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
        }
    }
}
+77 −0
Original line number Diff line number Diff line
/*
 * 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)
        }
    }
}
+42 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
Loading