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