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

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

Partial Screen Sharing: Task Switcher - Media Projection State Repo

Implement the repository that will expose the state of media projection
state.
This implementation uses the new method added to
MediaProjectionManager#Callback, that exposes ContentRecordingSession.

Bug: 286201261
Test: MediaProjectionManagerRepositoryTest.kt
Change-Id: Id1c3e56e929b1fd9f3d762157e6afc5da89063a7
parent 2cbdf89c
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -17,8 +17,8 @@
package com.android.systemui.mediaprojection.taskswitcher

import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository
import com.android.systemui.mediaprojection.taskswitcher.data.repository.NoOpMediaProjectionRepository
import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
import dagger.Binds
import dagger.Module
@@ -26,7 +26,7 @@ import dagger.Module
@Module
interface MediaProjectionTaskSwitcherModule {

    @Binds fun mediaRepository(impl: NoOpMediaProjectionRepository): MediaProjectionRepository
    @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository

    @Binds fun tasksRepository(impl: ActivityTaskManagerTasksRepository): TasksRepository
}
+92 −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.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.util.Log
import android.view.ContentRecordingSession
import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
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.Main
import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
import javax.inject.Inject
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.launch

@SysUISingleton
class MediaProjectionManagerRepository
@Inject
constructor(
    private val mediaProjectionManager: MediaProjectionManager,
    @Main private val handler: Handler,
    @Application private val applicationScope: CoroutineScope,
    private val tasksRepository: TasksRepository,
) : MediaProjectionRepository {

    override val mediaProjectionState: Flow<MediaProjectionState> =
        conflatedCallbackFlow {
                val callback =
                    object : MediaProjectionManager.Callback() {
                        override fun onStart(info: MediaProjectionInfo?) {
                            Log.d(TAG, "MediaProjectionManager.Callback#onStart")
                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
                        }

                        override fun onStop(info: MediaProjectionInfo?) {
                            Log.d(TAG, "MediaProjectionManager.Callback#onStop")
                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
                        }

                        override fun onRecordingSessionSet(
                            info: MediaProjectionInfo,
                            session: ContentRecordingSession?
                        ) {
                            Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
                            launch { trySendWithFailureLogging(stateForSession(session), TAG) }
                        }
                    }
                mediaProjectionManager.addCallback(callback, handler)
                awaitClose { mediaProjectionManager.removeCallback(callback) }
            }
            .shareIn(scope = applicationScope, started = SharingStarted.Lazily, replay = 1)

    private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState {
        if (session == null) {
            return MediaProjectionState.NotProjecting
        }
        if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
            return MediaProjectionState.EntireScreen
        }
        val matchingTask =
            tasksRepository.findRunningTaskFromWindowContainerToken(session.tokenToRecord)
                ?: return MediaProjectionState.EntireScreen
        return MediaProjectionState.SingleTask(matchingTask)
    }

    companion object {
        private const val TAG = "MediaProjectionMngrRepo"
    }
}
+68 −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.content.Intent
import android.os.IBinder
import android.window.IWindowContainerToken
import android.window.WindowContainerToken
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class FakeTasksRepository : TasksRepository {

    private val _foregroundTask = MutableStateFlow(DEFAULT_TASK)

    override val foregroundTask: Flow<RunningTaskInfo> = _foregroundTask.asStateFlow()

    private val runningTasks = mutableListOf(DEFAULT_TASK)

    override suspend fun findRunningTaskFromWindowContainerToken(
        windowContainerToken: IBinder
    ): RunningTaskInfo? = runningTasks.firstOrNull { it.token.asBinder() == windowContainerToken }

    fun addRunningTask(task: RunningTaskInfo) {
        runningTasks.add(task)
    }

    fun moveTaskToForeground(task: RunningTaskInfo) {
        _foregroundTask.value = task
    }

    companion object {
        val DEFAULT_TASK = createTask(taskId = -1)
        val LAUNCHER_INTENT: Intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)

        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)
        }
    }
}
+162 −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.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
import android.os.Binder
import android.os.Handler
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.view.ContentRecordingSession
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
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

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@SmallTest
class MediaProjectionManagerRepositoryTest : SysuiTestCase() {

    private val mediaProjectionManager = mock<MediaProjectionManager>()

    private val dispatcher = StandardTestDispatcher()
    private val testScope = TestScope(dispatcher)
    private val tasksRepo = FakeTasksRepository()

    private lateinit var callback: MediaProjectionManager.Callback
    private lateinit var repo: MediaProjectionManagerRepository

    @Before
    fun setUp() {
        whenever(mediaProjectionManager.addCallback(any(), any())).thenAnswer {
            callback = it.arguments[0] as MediaProjectionManager.Callback
            return@thenAnswer Unit
        }
        repo =
            MediaProjectionManagerRepository(
                mediaProjectionManager = mediaProjectionManager,
                handler = Handler.getMain(),
                applicationScope = testScope.backgroundScope,
                tasksRepository = tasksRepo
            )
    }

    @Test
    fun mediaProjectionState_onStart_emitsNotProjecting() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            callback.onStart(TEST_MEDIA_INFO)

            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
        }

    @Test
    fun mediaProjectionState_onStop_emitsNotProjecting() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            callback.onStop(TEST_MEDIA_INFO)

            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
        }

    @Test
    fun mediaProjectionState_onSessionSet_sessionNull_emitsNotProjecting() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            callback.onRecordingSessionSet(TEST_MEDIA_INFO, /* session= */ null)

            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
        }

    @Test
    fun mediaProjectionState_onSessionSet_contentToRecordDisplay_emitsEntireScreen() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)

            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
        }

    @Test
    fun mediaProjectionState_onSessionSet_tokenNull_emitsEntireScreen() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            val session =
                ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null)
            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)

            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
        }

    @Test
    fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() =
        testScope.runTest {
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            val taskWindowContainerToken = Binder()
            val session = ContentRecordingSession.createTaskSession(taskWindowContainerToken)
            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)

            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
        }

    @Test
    fun mediaProjectionState_sessionSet_taskWithToken_matchingRunningTask_emitsSingleTask() =
        testScope.runTest {
            val token = FakeTasksRepository.createToken()
            val task = FakeTasksRepository.createTask(taskId = 1, token = token)
            tasksRepo.addRunningTask(task)
            val state by collectLastValue(repo.mediaProjectionState)
            runCurrent()

            val session = ContentRecordingSession.createTaskSession(token.asBinder())
            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)

            assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
        }

    companion object {
        val TEST_MEDIA_INFO =
            MediaProjectionInfo(/* packageName= */ "com.test.package", UserHandle.CURRENT)
    }
}