• 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.statusbar.chips.screenrecord.domain.interactor
18 
19 import android.media.projection.StopReason
20 import com.android.systemui.Flags
21 import com.android.systemui.dagger.SysUISingleton
22 import com.android.systemui.dagger.qualifiers.Application
23 import com.android.systemui.log.LogBuffer
24 import com.android.systemui.log.core.LogLevel
25 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
26 import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
27 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
28 import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
29 import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
30 import com.android.systemui.statusbar.chips.StatusBarChipsLog
31 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
32 import javax.inject.Inject
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.delay
35 import kotlinx.coroutines.flow.Flow
36 import kotlinx.coroutines.flow.SharingStarted
37 import kotlinx.coroutines.flow.StateFlow
38 import kotlinx.coroutines.flow.combine
39 import kotlinx.coroutines.flow.stateIn
40 import kotlinx.coroutines.flow.transformLatest
41 import kotlinx.coroutines.launch
42 
43 /** Interactor for the screen recording chip shown in the status bar. */
44 @SysUISingleton
45 class ScreenRecordChipInteractor
46 @Inject
47 constructor(
48     @Application private val scope: CoroutineScope,
49     private val screenRecordRepository: ScreenRecordRepository,
50     private val mediaProjectionRepository: MediaProjectionRepository,
51     @StatusBarChipsLog private val logger: LogBuffer,
52 ) {
53     /**
54      * Emits true if we should assume that we're currently screen recording, even if
55      * [ScreenRecordRepository.screenRecordState] hasn't emitted [ScreenRecordModel.Recording] yet.
56      */
57     private val shouldAssumeIsRecording: Flow<Boolean> =
58         screenRecordRepository.screenRecordState
<lambda>null59             .transformLatest {
60                 when (it) {
61                     is ScreenRecordModel.DoingNothing -> {
62                         emit(false)
63                     }
64                     is ScreenRecordModel.Starting -> {
65                         // If we're told that the recording will start in [it.millisUntilStarted],
66                         // optimistically assume the recording did indeed start after that time even
67                         // if [ScreenRecordRepository.screenRecordState] hasn't emitted
68                         // [ScreenRecordModel.Recording] yet. Start 50ms early so that the chip
69                         // timer will definitely be showing by the time the recording actually
70                         // starts - see b/366448907.
71                         delay(it.millisUntilStarted - 50)
72                         emit(true)
73                     }
74                     is ScreenRecordModel.Recording -> {}
75                 }
76             }
77             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
78 
79     val screenRecordState: StateFlow<ScreenRecordChipModel> =
80         // ScreenRecordRepository has the main "is the screen being recorded?" state, and
81         // MediaProjectionRepository has information about what specifically is being recorded (a
82         // single app or the entire screen)
83         combine(
84                 screenRecordRepository.screenRecordState,
85                 mediaProjectionRepository.mediaProjectionState,
86                 shouldAssumeIsRecording,
87             ) { screenRecordState, mediaProjectionState, shouldAssumeIsRecording ->
88                 if (
89                     Flags.statusBarAutoStartScreenRecordChip() &&
90                         shouldAssumeIsRecording &&
91                         screenRecordState is ScreenRecordModel.Starting
92                 ) {
93                     logger.log(
94                         TAG,
95                         LogLevel.INFO,
<lambda>null96                         {},
<lambda>null97                         {
98                             "State: Recording(hostPackage=null, taskPackage=null) due to force-start"
99                         },
100                     )
101                     ScreenRecordChipModel.Recording(hostPackage = null, recordedTask = null)
102                 } else {
103                     when (screenRecordState) {
104                         is ScreenRecordModel.DoingNothing -> {
<lambda>null105                             logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" })
106                             ScreenRecordChipModel.DoingNothing
107                         }
108 
109                         is ScreenRecordModel.Starting -> {
110                             logger.log(
111                                 TAG,
112                                 LogLevel.INFO,
<lambda>null113                                 { long1 = screenRecordState.millisUntilStarted },
<lambda>null114                                 { "State: Starting($long1)" },
115                             )
116                             ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
117                         }
118 
119                         is ScreenRecordModel.Recording -> {
120                             val recordedTask =
121                                 if (
122                                     mediaProjectionState
123                                         is MediaProjectionState.Projecting.SingleTask
124                                 ) {
125                                     mediaProjectionState.task
126                                 } else {
127                                     null
128                                 }
129                             val hostPackage =
130                                 if (mediaProjectionState is MediaProjectionState.Projecting) {
131                                     mediaProjectionState.hostPackage
132                                 } else {
133                                     null
134                                 }
135                             logger.log(
136                                 TAG,
137                                 LogLevel.INFO,
<lambda>null138                                 {
139                                     str1 = hostPackage
140                                     str2 = recordedTask?.baseIntent?.component?.packageName
141                                 },
<lambda>null142                                 { "State: Recording(hostPackage=$str1, taskPackage=$str2)" },
143                             )
144                             ScreenRecordChipModel.Recording(
145                                 hostPackage = hostPackage,
146                                 recordedTask = recordedTask,
147                             )
148                         }
149                     }
150                 }
151             }
152             .stateIn(scope, SharingStarted.WhileSubscribed(), ScreenRecordChipModel.DoingNothing)
153 
154     /** Stops the recording. */
stopRecordingnull155     fun stopRecording() {
156         scope.launch { screenRecordRepository.stopRecording(StopReason.STOP_PRIVACY_CHIP) }
157     }
158 
159     companion object {
160         private val TAG = "ScreenRecord".pad()
161     }
162 }
163