Loading packages/SystemUI/aconfig/systemui.aconfig +11 −0 Original line number Diff line number Diff line Loading @@ -413,6 +413,17 @@ flag { } } flag { name: "status_bar_auto_start_screen_record_chip" namespace: "systemui" description: "When screen recording, use the specified start time to update the screen record " "chip state instead of waiting for an official 'recording started' signal" bug: "366448907" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "status_bar_use_repos_for_call_chip" namespace: "systemui" Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +140 −3 Original line number Diff line number Diff line Loading @@ -16,29 +16,35 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class ScreenRecordChipInteractorTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val screenRecordRepo = kosmos.screenRecordRepository private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository Loading Loading @@ -115,6 +121,137 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) } @Test @DisableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_flagOff_doesNotAutomaticallySwitchToRecordingBasedOnTime() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN screen record should start in 900ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) // WHEN 900ms has elapsed advanceTimeBy(901) // THEN we don't automatically update to the recording state if the flag is off assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_flagOn_automaticallySwitchesToRecordingBasedOnTime() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN screen record should start in 900ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) // WHEN 900ms has elapsed advanceTimeBy(901) // THEN we automatically update to the recording state assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_recordingBeginsEarly_switchesToRecording() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN screen record should start in 900ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) // WHEN we update to the Recording state earlier than 900ms advanceTimeBy(800) screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording val task = createTask(taskId = 1) mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, task, ) // THEN we immediately switch to Recording, and we have the task assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) // WHEN more than 900ms has elapsed advanceTimeBy(200) // THEN we still stay in the Recording state and we have the task assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_secondRecording_doesNotAutomaticallyStart() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // First recording starts, records, and stops screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) advanceTimeBy(900) screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording advanceTimeBy(5000) screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing advanceTimeBy(10000) assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing) // WHEN a second recording is starting screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900) // THEN we stay as starting and do not switch to Recording (verifying the auto-start // timer is reset) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_startingButThenDoingNothing_doesNotAutomaticallyStart() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN a screen recording is starting in 500ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500)) // But it's cancelled after 300ms advanceTimeBy(300) screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing // THEN we don't automatically start the recording 200ms later advanceTimeBy(201) assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_multipleStartingValues_autoStartResets() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900)) advanceTimeBy(2800) // WHEN there's 100ms left to go before auto-start, but then we get a new start time // that's in 500ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500) // THEN we don't auto-start in 100ms advanceTimeBy(101) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500)) // THEN we *do* auto-start 400ms later advanceTimeBy(401) assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) } @Test fun stopRecording_sendsToRepo() = testScope.runTest { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +9 −29 Original line number Diff line number Diff line Loading @@ -26,9 +26,8 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager Loading @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test Loading @@ -61,7 +61,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ScreenRecordChipViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val screenRecordRepo = kosmos.screenRecordRepository private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository Loading Loading @@ -254,7 +254,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, FakeActivityTaskManager.createTask(taskId = 1) FakeActivityTaskManager.createTask(taskId = 1), ) // THEN the start time is still the old start time Loading @@ -275,12 +275,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) .showFromView( eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean(), ) .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test Loading @@ -297,12 +292,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) .showFromView( eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean(), ) .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test Loading @@ -314,7 +304,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, FakeActivityTaskManager.createTask(taskId = 1) FakeActivityTaskManager.createTask(taskId = 1), ) val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) Loading @@ -323,12 +313,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) .showFromView( eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean(), ) .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test Loading @@ -344,12 +329,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val cujCaptor = argumentCaptor<DialogCuj>() verify(kosmos.mockDialogTransitionAnimator) .showFromView( any(), any(), cujCaptor.capture(), anyBoolean(), ) .showFromView(any(), any(), cujCaptor.capture(), anyBoolean()) assertThat(cujCaptor.firstValue.cujType) .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +2 −2 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository Loading @@ -48,6 +47,7 @@ import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi Loading @@ -72,7 +72,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @DisableFlags(StatusBarNotifChips.FLAG_NAME) class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val kosmos = testKosmos() private val testScope = kosmos.testScope private val systemClock = kosmos.fakeSystemClock Loading packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +82 −32 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer Loading @@ -28,14 +29,19 @@ import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch /** Interactor for the screen recording chip shown in the status bar. */ @SysUISingleton @OptIn(ExperimentalCoroutinesApi::class) class ScreenRecordChipInteractor @Inject constructor( Loading @@ -44,6 +50,32 @@ constructor( private val mediaProjectionRepository: MediaProjectionRepository, @StatusBarChipsLog private val logger: LogBuffer, ) { /** * Emits true if we should assume that we're currently screen recording, even if * [ScreenRecordRepository.screenRecordState] hasn't emitted [ScreenRecordModel.Recording] yet. */ private val shouldAssumeIsRecording: Flow<Boolean> = screenRecordRepository.screenRecordState .transformLatest { when (it) { is ScreenRecordModel.DoingNothing -> { emit(false) } is ScreenRecordModel.Starting -> { // If we're told that the recording will start in [it.millisUntilStarted], // optimistically assume the recording did indeed start after that time even // if [ScreenRecordRepository.screenRecordState] hasn't emitted // [ScreenRecordModel.Recording] yet. Start 50ms early so that the chip // timer will definitely be showing by the time the recording actually // starts - see b/366448907. delay(it.millisUntilStarted - 50) emit(true) } is ScreenRecordModel.Recording -> {} } } .stateIn(scope, SharingStarted.WhileSubscribed(), false) val screenRecordState: StateFlow<ScreenRecordChipModel> = // ScreenRecordRepository has the main "is the screen being recorded?" state, and // MediaProjectionRepository has information about what specifically is being recorded (a Loading @@ -51,25 +83,42 @@ constructor( combine( screenRecordRepository.screenRecordState, mediaProjectionRepository.mediaProjectionState, ) { screenRecordState, mediaProjectionState -> shouldAssumeIsRecording, ) { screenRecordState, mediaProjectionState, shouldAssumeIsRecording -> if ( Flags.statusBarAutoStartScreenRecordChip() && shouldAssumeIsRecording && screenRecordState is ScreenRecordModel.Starting ) { logger.log( TAG, LogLevel.INFO, {}, { "State: Recording(taskPackage=null) due to force-start" }, ) ScreenRecordChipModel.Recording(recordedTask = null) } else { when (screenRecordState) { is ScreenRecordModel.DoingNothing -> { logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" }) ScreenRecordChipModel.DoingNothing } is ScreenRecordModel.Starting -> { logger.log( TAG, LogLevel.INFO, { long1 = screenRecordState.millisUntilStarted }, { "State: Starting($long1)" } { "State: Starting($long1)" }, ) ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted) } is ScreenRecordModel.Recording -> { val recordedTask = if ( mediaProjectionState is MediaProjectionState.Projecting.SingleTask mediaProjectionState is MediaProjectionState.Projecting.SingleTask ) { mediaProjectionState.task } else { Loading @@ -79,12 +128,13 @@ constructor( TAG, LogLevel.INFO, { str1 = recordedTask?.baseIntent?.component?.packageName }, { "State: Recording(taskPackage=$str1)" } { "State: Recording(taskPackage=$str1)" }, ) ScreenRecordChipModel.Recording(recordedTask) } } } } .stateIn(scope, SharingStarted.WhileSubscribed(), ScreenRecordChipModel.DoingNothing) /** Stops the recording. */ Loading Loading
packages/SystemUI/aconfig/systemui.aconfig +11 −0 Original line number Diff line number Diff line Loading @@ -413,6 +413,17 @@ flag { } } flag { name: "status_bar_auto_start_screen_record_chip" namespace: "systemui" description: "When screen recording, use the specified start time to update the screen record " "chip state instead of waiting for an official 'recording started' signal" bug: "366448907" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "status_bar_use_repos_for_call_chip" namespace: "systemui" Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +140 −3 Original line number Diff line number Diff line Loading @@ -16,29 +16,35 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class ScreenRecordChipInteractorTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val screenRecordRepo = kosmos.screenRecordRepository private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository Loading Loading @@ -115,6 +121,137 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) } @Test @DisableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_flagOff_doesNotAutomaticallySwitchToRecordingBasedOnTime() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN screen record should start in 900ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) // WHEN 900ms has elapsed advanceTimeBy(901) // THEN we don't automatically update to the recording state if the flag is off assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_flagOn_automaticallySwitchesToRecordingBasedOnTime() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN screen record should start in 900ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) // WHEN 900ms has elapsed advanceTimeBy(901) // THEN we automatically update to the recording state assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_recordingBeginsEarly_switchesToRecording() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN screen record should start in 900ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) // WHEN we update to the Recording state earlier than 900ms advanceTimeBy(800) screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording val task = createTask(taskId = 1) mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, task, ) // THEN we immediately switch to Recording, and we have the task assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) // WHEN more than 900ms has elapsed advanceTimeBy(200) // THEN we still stay in the Recording state and we have the task assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_secondRecording_doesNotAutomaticallyStart() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // First recording starts, records, and stops screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) advanceTimeBy(900) screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording advanceTimeBy(5000) screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing advanceTimeBy(10000) assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing) // WHEN a second recording is starting screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900) // THEN we stay as starting and do not switch to Recording (verifying the auto-start // timer is reset) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900)) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_startingButThenDoingNothing_doesNotAutomaticallyStart() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) // WHEN a screen recording is starting in 500ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500)) // But it's cancelled after 300ms advanceTimeBy(300) screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing // THEN we don't automatically start the recording 200ms later advanceTimeBy(201) assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing) } @Test @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) fun screenRecordState_multipleStartingValues_autoStartResets() = testScope.runTest { val latest by collectLastValue(underTest.screenRecordState) screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900)) advanceTimeBy(2800) // WHEN there's 100ms left to go before auto-start, but then we get a new start time // that's in 500ms screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500) // THEN we don't auto-start in 100ms advanceTimeBy(101) assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500)) // THEN we *do* auto-start 400ms later advanceTimeBy(401) assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) } @Test fun stopRecording_sendsToRepo() = testScope.runTest { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +9 −29 Original line number Diff line number Diff line Loading @@ -26,9 +26,8 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager Loading @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test Loading @@ -61,7 +61,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ScreenRecordChipViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val screenRecordRepo = kosmos.screenRecordRepository private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository Loading Loading @@ -254,7 +254,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, FakeActivityTaskManager.createTask(taskId = 1) FakeActivityTaskManager.createTask(taskId = 1), ) // THEN the start time is still the old start time Loading @@ -275,12 +275,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) .showFromView( eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean(), ) .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test Loading @@ -297,12 +292,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) .showFromView( eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean(), ) .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test Loading @@ -314,7 +304,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, FakeActivityTaskManager.createTask(taskId = 1) FakeActivityTaskManager.createTask(taskId = 1), ) val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) Loading @@ -323,12 +313,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) .showFromView( eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean(), ) .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test Loading @@ -344,12 +329,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val cujCaptor = argumentCaptor<DialogCuj>() verify(kosmos.mockDialogTransitionAnimator) .showFromView( any(), any(), cujCaptor.capture(), anyBoolean(), ) .showFromView(any(), any(), cujCaptor.capture(), anyBoolean()) assertThat(cujCaptor.firstValue.cujType) .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +2 −2 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository Loading @@ -48,6 +47,7 @@ import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi Loading @@ -72,7 +72,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @DisableFlags(StatusBarNotifChips.FLAG_NAME) class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val kosmos = testKosmos() private val testScope = kosmos.testScope private val systemClock = kosmos.fakeSystemClock Loading
packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +82 −32 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer Loading @@ -28,14 +29,19 @@ import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch /** Interactor for the screen recording chip shown in the status bar. */ @SysUISingleton @OptIn(ExperimentalCoroutinesApi::class) class ScreenRecordChipInteractor @Inject constructor( Loading @@ -44,6 +50,32 @@ constructor( private val mediaProjectionRepository: MediaProjectionRepository, @StatusBarChipsLog private val logger: LogBuffer, ) { /** * Emits true if we should assume that we're currently screen recording, even if * [ScreenRecordRepository.screenRecordState] hasn't emitted [ScreenRecordModel.Recording] yet. */ private val shouldAssumeIsRecording: Flow<Boolean> = screenRecordRepository.screenRecordState .transformLatest { when (it) { is ScreenRecordModel.DoingNothing -> { emit(false) } is ScreenRecordModel.Starting -> { // If we're told that the recording will start in [it.millisUntilStarted], // optimistically assume the recording did indeed start after that time even // if [ScreenRecordRepository.screenRecordState] hasn't emitted // [ScreenRecordModel.Recording] yet. Start 50ms early so that the chip // timer will definitely be showing by the time the recording actually // starts - see b/366448907. delay(it.millisUntilStarted - 50) emit(true) } is ScreenRecordModel.Recording -> {} } } .stateIn(scope, SharingStarted.WhileSubscribed(), false) val screenRecordState: StateFlow<ScreenRecordChipModel> = // ScreenRecordRepository has the main "is the screen being recorded?" state, and // MediaProjectionRepository has information about what specifically is being recorded (a Loading @@ -51,25 +83,42 @@ constructor( combine( screenRecordRepository.screenRecordState, mediaProjectionRepository.mediaProjectionState, ) { screenRecordState, mediaProjectionState -> shouldAssumeIsRecording, ) { screenRecordState, mediaProjectionState, shouldAssumeIsRecording -> if ( Flags.statusBarAutoStartScreenRecordChip() && shouldAssumeIsRecording && screenRecordState is ScreenRecordModel.Starting ) { logger.log( TAG, LogLevel.INFO, {}, { "State: Recording(taskPackage=null) due to force-start" }, ) ScreenRecordChipModel.Recording(recordedTask = null) } else { when (screenRecordState) { is ScreenRecordModel.DoingNothing -> { logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" }) ScreenRecordChipModel.DoingNothing } is ScreenRecordModel.Starting -> { logger.log( TAG, LogLevel.INFO, { long1 = screenRecordState.millisUntilStarted }, { "State: Starting($long1)" } { "State: Starting($long1)" }, ) ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted) } is ScreenRecordModel.Recording -> { val recordedTask = if ( mediaProjectionState is MediaProjectionState.Projecting.SingleTask mediaProjectionState is MediaProjectionState.Projecting.SingleTask ) { mediaProjectionState.task } else { Loading @@ -79,12 +128,13 @@ constructor( TAG, LogLevel.INFO, { str1 = recordedTask?.baseIntent?.component?.packageName }, { "State: Recording(taskPackage=$str1)" } { "State: Recording(taskPackage=$str1)" }, ) ScreenRecordChipModel.Recording(recordedTask) } } } } .stateIn(scope, SharingStarted.WhileSubscribed(), ScreenRecordChipModel.DoingNothing) /** Stops the recording. */ Loading