• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.hardware.display.DisplayManager
21 import android.media.projection.MediaProjectionEvent
22 import android.media.projection.MediaProjectionInfo
23 import android.media.projection.MediaProjectionManager
24 import android.media.projection.StopReason
25 import android.os.Handler
26 import android.view.ContentRecordingSession
27 import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
28 import com.android.systemui.Flags
29 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.dagger.qualifiers.Background
33 import com.android.systemui.dagger.qualifiers.Main
34 import com.android.systemui.log.LogBuffer
35 import com.android.systemui.log.core.LogLevel
36 import com.android.systemui.mediaprojection.MediaProjectionLog
37 import com.android.systemui.mediaprojection.MediaProjectionServiceHelper
38 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
39 import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
40 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
41 import javax.inject.Inject
42 import kotlinx.coroutines.CoroutineDispatcher
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.channels.awaitClose
45 import kotlinx.coroutines.flow.Flow
46 import kotlinx.coroutines.flow.SharingStarted
47 import kotlinx.coroutines.flow.filter
48 import kotlinx.coroutines.flow.filterNot
49 import kotlinx.coroutines.flow.map
50 import kotlinx.coroutines.flow.mapLatest
51 import kotlinx.coroutines.flow.stateIn
52 import kotlinx.coroutines.withContext
53 
54 @SysUISingleton
55 class MediaProjectionManagerRepository
56 @Inject
57 constructor(
58     private val mediaProjectionManager: MediaProjectionManager,
59     private val displayManager: DisplayManager,
60     @Main private val handler: Handler,
61     @Application private val applicationScope: CoroutineScope,
62     @Background private val backgroundDispatcher: CoroutineDispatcher,
63     private val tasksRepository: TasksRepository,
64     private val mediaProjectionServiceHelper: MediaProjectionServiceHelper,
65     @MediaProjectionLog private val logger: LogBuffer,
66 ) : MediaProjectionRepository {
67 
switchProjectedTasknull68     override suspend fun switchProjectedTask(task: RunningTaskInfo) {
69         withContext(backgroundDispatcher) {
70             if (mediaProjectionServiceHelper.updateTaskRecordingSession(task.token)) {
71                 logger.log(TAG, LogLevel.DEBUG, {}, { "Successfully switched projected task" })
72             } else {
73                 logger.log(TAG, LogLevel.WARNING, {}, { "Failed to switch projected task" })
74             }
75         }
76     }
77 
stopProjectingnull78     override suspend fun stopProjecting(@StopReason stopReason: Int) {
79         withContext(backgroundDispatcher) {
80             logger.log(
81                 TAG,
82                 LogLevel.DEBUG,
83                 {},
84                 { "Requesting MediaProjectionManager#stopActiveProjection" },
85             )
86             mediaProjectionManager.stopActiveProjection(stopReason)
87         }
88     }
89 
<lambda>null90     private val callbackEventsFlow = conflatedCallbackFlow {
91         val callback =
92             object : MediaProjectionManager.Callback() {
93                 override fun onStart(info: MediaProjectionInfo?) {
94                     logger.log(TAG, LogLevel.DEBUG, {}, { "Callback#onStart" })
95                     trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG)
96                 }
97 
98                 override fun onStop(info: MediaProjectionInfo?) {
99                     logger.log(TAG, LogLevel.DEBUG, {}, { "Callback#onStop" })
100                     trySendWithFailureLogging(CallbackEvent.OnStop, TAG)
101                 }
102 
103                 override fun onRecordingSessionSet(
104                     info: MediaProjectionInfo,
105                     session: ContentRecordingSession?,
106                 ) {
107                     logger.log(
108                         TAG,
109                         LogLevel.DEBUG,
110                         { str1 = session.toString() },
111                         { "Callback#onSessionSet: $str1" },
112                     )
113                     trySendWithFailureLogging(
114                         CallbackEvent.OnRecordingSessionSet(info, session),
115                         TAG,
116                     )
117                 }
118 
119                 override fun onMediaProjectionEvent(
120                     event: MediaProjectionEvent,
121                     info: MediaProjectionInfo?,
122                     session: ContentRecordingSession?,
123                 ) {
124                     if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
125                         logger.log(
126                             TAG,
127                             LogLevel.DEBUG,
128                             { str1 = event.toString() },
129                             { "Callback#onMediaProjectionEvent : $str1" },
130                         )
131                         trySendWithFailureLogging(CallbackEvent.OnMediaProjectionEvent(event), TAG)
132                     }
133                 }
134             }
135         mediaProjectionManager.addCallback(callback, handler)
136         awaitClose { mediaProjectionManager.removeCallback(callback) }
137     }
138 
139     override val mediaProjectionState: Flow<MediaProjectionState> =
140         callbackEventsFlow
<lambda>null141             .filterNot {
142                 it is CallbackEvent.OnMediaProjectionEvent // Exclude OnMediaProjectionEvent
143             }
144             // When we get an #onRecordingSessionSet event, we need to do some work in the
145             // background before emitting the right state value. But when we get an #onStop
146             // event, we immediately know what state value to emit.
147             //
148             // Without `mapLatest`, this could be a problem if an #onRecordingSessionSet event
149             // comes in and then an #onStop event comes in shortly afterwards (b/352483752):
150             // 1. #onRecordingSessionSet -> start some work in the background
151             // 2. #onStop -> immediately emit "Not Projecting"
152             // 3. onRecordingSessionSet work finishes -> emit "Projecting"
153             //
154             // At step 3, we *shouldn't* emit "Projecting" because #onStop was the last callback
155             // event we received, so we should be "Not Projecting". This `mapLatest` ensures
156             // that if an #onStop event comes in, we cancel any ongoing work for
157             // #onRecordingSessionSet and we don't emit "Projecting".
<lambda>null158             .mapLatest {
159                 when (it) {
160                     is CallbackEvent.OnStart -> {
161                         if (!Flags.statusBarShowAudioOnlyProjectionChip()) {
162                             return@mapLatest MediaProjectionState.NotProjecting
163                         }
164                         // It's possible for a projection to be audio-only, in which case `OnStart`
165                         // will occur but `OnRecordingSessionSet` will not. We should still consider
166                         // us to be projecting even if only audio is projecting. See b/373308507.
167                         if (it.info != null) {
168                             MediaProjectionState.Projecting.NoScreen(
169                                 hostPackage = it.info.packageName
170                             )
171                         } else {
172                             MediaProjectionState.NotProjecting
173                         }
174                     }
175                     is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting
176                     is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session)
177                     is CallbackEvent.OnMediaProjectionEvent ->
178                         throw IllegalStateException(
179                             "Unexpected OnMediaProjectionEvent in mediaProjectionState flow. It " +
180                                 "should have been filtered out."
181                         )
182                 }
183             }
184             .stateIn(
185                 scope = applicationScope,
186                 started = SharingStarted.Lazily,
187                 initialValue = MediaProjectionState.NotProjecting,
188             )
189 
190     override val projectionStartedDuringCallAndActivePostCallEvent: Flow<Unit> =
191         callbackEventsFlow
<lambda>null192             .filter {
193                 com.android.media.projection.flags.Flags.showStopDialogPostCallEnd() &&
194                     it is CallbackEvent.OnMediaProjectionEvent &&
195                     it.event.eventType ==
196                         MediaProjectionEvent.PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL
197             }
<lambda>null198             .map {}
199 
stateForSessionnull200     private suspend fun stateForSession(
201         info: MediaProjectionInfo,
202         session: ContentRecordingSession?,
203     ): MediaProjectionState {
204         if (session == null) {
205             return MediaProjectionState.NotProjecting
206         }
207 
208         val hostPackage = info.packageName
209         val hostDeviceName =
210             withContext(backgroundDispatcher) {
211                 // If the projection is to a different device, then the session's display ID should
212                 // identify the display associated with that different device.
213                 displayManager.getDisplay(session.virtualDisplayId)?.name
214             }
215 
216         if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
217             return MediaProjectionState.Projecting.EntireScreen(hostPackage, hostDeviceName)
218         }
219         val matchingTask =
220             tasksRepository.findRunningTaskFromWindowContainerToken(
221                 checkNotNull(session.tokenToRecord)
222             ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage, hostDeviceName)
223         return MediaProjectionState.Projecting.SingleTask(hostPackage, hostDeviceName, matchingTask)
224     }
225 
226     /**
227      * Translates [MediaProjectionManager.Callback] events into objects so that we always maintain
228      * the correct callback ordering.
229      */
230     sealed interface CallbackEvent {
231         data class OnStart(val info: MediaProjectionInfo?) : CallbackEvent
232 
233         data object OnStop : CallbackEvent
234 
235         data class OnRecordingSessionSet(
236             val info: MediaProjectionInfo,
237             val session: ContentRecordingSession?,
238         ) : CallbackEvent
239 
240         data class OnMediaProjectionEvent(val event: MediaProjectionEvent) : CallbackEvent
241     }
242 
243     companion object {
244         private const val TAG = "MediaProjectionMngrRepo"
245     }
246 }
247