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