diff options
| author | 2024-11-07 14:33:48 +0000 | |
|---|---|---|
| committer | 2024-11-07 14:33:48 +0000 | |
| commit | a997b8ed2710a54b507d31eb1a641da238de1da1 (patch) | |
| tree | fa4b058614aba8a35d736f081390cd4f26e3604e | |
| parent | 5b685ed0775cc4efadcce9cf6d32129689908ae1 (diff) | |
| parent | 51ef42f7a718846302ed4b74f8ab9cd3548b39af (diff) | |
Merge "[SB][Screen share] Proactively start screen record timer." into main
5 files changed, 244 insertions, 66 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 1c846a613dd4..5eae6d3e43fe 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -414,6 +414,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" description: "Use repositories as the source of truth for call notifications shown as a chip in" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt index 0efd591940f2..11a125a21be0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt @@ -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 @@ -116,6 +122,137 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { } @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 { assertThat(screenRecordRepo.stopRecordingInvoked).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index bfebe184ae2d..48d8add6b33a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 @@ -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) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index e96def6d43a3..c5c2a94cf0ea 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -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 @@ -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 @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt index 9c53cc13f702..e3dc70af5fe6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt @@ -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 @@ -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( @@ -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 @@ -51,37 +83,55 @@ constructor( combine( screenRecordRepository.screenRecordState, mediaProjectionRepository.mediaProjectionState, - ) { screenRecordState, mediaProjectionState -> - 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)" } - ) - ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted) - } - is ScreenRecordModel.Recording -> { - val recordedTask = - if ( - mediaProjectionState is MediaProjectionState.Projecting.SingleTask - ) { - mediaProjectionState.task - } else { - null - } - logger.log( - TAG, - LogLevel.INFO, - { str1 = recordedTask?.baseIntent?.component?.packageName }, - { "State: Recording(taskPackage=$str1)" } - ) - ScreenRecordChipModel.Recording(recordedTask) + 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)" }, + ) + ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted) + } + + is ScreenRecordModel.Recording -> { + val recordedTask = + if ( + mediaProjectionState + is MediaProjectionState.Projecting.SingleTask + ) { + mediaProjectionState.task + } else { + null + } + logger.log( + TAG, + LogLevel.INFO, + { str1 = recordedTask?.baseIntent?.component?.packageName }, + { "State: Recording(taskPackage=$str1)" }, + ) + ScreenRecordChipModel.Recording(recordedTask) + } } } } |