Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a997b8ed authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[SB][Screen share] Proactively start screen record timer." into main

parents 5b685ed0 51ef42f7
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -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"
+140 −3
Original line number Diff line number Diff line
@@ -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
@@ -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 {
+9 −29
Original line number Diff line number Diff line
@@ -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)
+2 −2
Original line number Diff line number Diff line
@@ -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

+82 −32
Original line number Diff line number Diff line
@@ -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,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 {
@@ -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. */