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:
committed by
Chris Göllner
parent
2cbdf89c36
commit
09187289ae
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user