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
This commit is contained in:
Christian Göllner
2023-06-22 17:13:50 +02:00
committed by Chris Göllner
parent 2cbdf89c36
commit 09187289ae
4 changed files with 324 additions and 2 deletions

View File

@@ -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
}

View File

@@ -0,0 +1,92 @@
/*
* 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"
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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)
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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)
}
}