1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.mediaprojection.data.repository 18 19 import android.app.ActivityManager.RunningTaskInfo 20 import android.media.projection.MediaProjectionInfo 21 import android.media.projection.MediaProjectionManager 22 import android.os.Handler 23 import android.util.Log 24 import android.view.ContentRecordingSession 25 import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY 26 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 27 import com.android.systemui.dagger.SysUISingleton 28 import com.android.systemui.dagger.qualifiers.Application 29 import com.android.systemui.dagger.qualifiers.Background 30 import com.android.systemui.dagger.qualifiers.Main 31 import com.android.systemui.mediaprojection.MediaProjectionServiceHelper 32 import com.android.systemui.mediaprojection.data.model.MediaProjectionState 33 import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository 34 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 35 import javax.inject.Inject 36 import kotlinx.coroutines.CoroutineDispatcher 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.channels.awaitClose 39 import kotlinx.coroutines.flow.Flow 40 import kotlinx.coroutines.flow.SharingStarted 41 import kotlinx.coroutines.flow.stateIn 42 import kotlinx.coroutines.launch 43 import kotlinx.coroutines.withContext 44 45 @SysUISingleton 46 class MediaProjectionManagerRepository 47 @Inject 48 constructor( 49 private val mediaProjectionManager: MediaProjectionManager, 50 @Main private val handler: Handler, 51 @Application private val applicationScope: CoroutineScope, 52 @Background private val backgroundDispatcher: CoroutineDispatcher, 53 private val tasksRepository: TasksRepository, 54 private val mediaProjectionServiceHelper: MediaProjectionServiceHelper, 55 ) : MediaProjectionRepository { 56 switchProjectedTasknull57 override suspend fun switchProjectedTask(task: RunningTaskInfo) { 58 withContext(backgroundDispatcher) { 59 if (mediaProjectionServiceHelper.updateTaskRecordingSession(task.token)) { 60 Log.d(TAG, "Successfully switched projected task") 61 } else { 62 Log.d(TAG, "Failed to switch projected task") 63 } 64 } 65 } 66 stopProjectingnull67 override suspend fun stopProjecting() { 68 withContext(backgroundDispatcher) { mediaProjectionManager.stopActiveProjection() } 69 } 70 71 override val mediaProjectionState: Flow<MediaProjectionState> = <lambda>null72 conflatedCallbackFlow { 73 val callback = 74 object : MediaProjectionManager.Callback() { 75 override fun onStart(info: MediaProjectionInfo?) { 76 Log.d(TAG, "MediaProjectionManager.Callback#onStart") 77 trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG) 78 } 79 80 override fun onStop(info: MediaProjectionInfo?) { 81 Log.d(TAG, "MediaProjectionManager.Callback#onStop") 82 trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG) 83 } 84 85 override fun onRecordingSessionSet( 86 info: MediaProjectionInfo, 87 session: ContentRecordingSession? 88 ) { 89 Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session") 90 launch { 91 trySendWithFailureLogging(stateForSession(info, session), TAG) 92 } 93 } 94 } 95 mediaProjectionManager.addCallback(callback, handler) 96 awaitClose { mediaProjectionManager.removeCallback(callback) } 97 } 98 .stateIn( 99 scope = applicationScope, 100 started = SharingStarted.Lazily, 101 initialValue = MediaProjectionState.NotProjecting, 102 ) 103 stateForSessionnull104 private suspend fun stateForSession( 105 info: MediaProjectionInfo, 106 session: ContentRecordingSession? 107 ): MediaProjectionState { 108 if (session == null) { 109 return MediaProjectionState.NotProjecting 110 } 111 112 val hostPackage = info.packageName 113 if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) { 114 return MediaProjectionState.Projecting.EntireScreen(hostPackage) 115 } 116 val matchingTask = 117 tasksRepository.findRunningTaskFromWindowContainerToken( 118 checkNotNull(session.tokenToRecord) 119 ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage) 120 return MediaProjectionState.Projecting.SingleTask(hostPackage, matchingTask) 121 } 122 123 companion object { 124 private const val TAG = "MediaProjectionMngrRepo" 125 } 126 } 127