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

Commit e3ea339c authored by Behnam Heydarshahi's avatar Behnam Heydarshahi
Browse files

Migrate ScreenRecord Tile

Flag: aconfig com.android.systemui.qs_new_tiles_future DEVELOPMENT
Flag: aconfig com.android.systemui.qs_new_tiles STAGING
Fixes: 301055674
Test: atest ScreenRecordTileUserActionInteractorTest
ScreenRecordTileDataInteractorTest ScreenRecordTileMapperTest

Change-Id: Ie7dd03ad7e4826e40865aa6a360d8faf87c594e8
parent 8756fdba
Loading
Loading
Loading
Loading
+149 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor

import android.os.UserHandle
import android.platform.test.annotations.EnabledOnRavenwood
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.impl.screenrecord.domain.model.ScreenRecordTileModel
import com.android.systemui.screenrecord.RecordingController
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@EnabledOnRavenwood
@RunWith(AndroidJUnit4::class)
class ScreenRecordTileDataInteractorTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private val controller = mock<RecordingController>()
    private val underTest: ScreenRecordTileDataInteractor =
        ScreenRecordTileDataInteractor(testScope.testScheduler, controller)

    private val isRecording = ScreenRecordTileModel.Recording
    private val isDoingNothing = ScreenRecordTileModel.DoingNothing
    private val isStarting0 = ScreenRecordTileModel.Starting(0)

    @Test
    fun isAvailable_returnsTrue() = runTest {
        val availability by collectLastValue(underTest.availability(TEST_USER))

        assertThat(availability).isTrue()
    }

    @Test
    fun dataMatchesController() =
        testScope.runTest {
            whenever(controller.isRecording).thenReturn(false)
            whenever(controller.isStarting).thenReturn(false)

            val callbackCaptor = argumentCaptor<RecordingController.RecordingStateChangeCallback>()

            val lastModel by
                collectLastValue(
                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
                )
            runCurrent()

            verify(controller).addCallback(callbackCaptor.capture())
            val callback = callbackCaptor.value

            assertThat(lastModel).isEqualTo(isDoingNothing)

            val expectedModelStartingIn1 = ScreenRecordTileModel.Starting(1)
            callback.onCountdown(1)
            assertThat(lastModel).isEqualTo(expectedModelStartingIn1)

            val expectedModelStartingIn0 = isStarting0
            callback.onCountdown(0)
            assertThat(lastModel).isEqualTo(expectedModelStartingIn0)

            callback.onCountdownEnd()
            assertThat(lastModel).isEqualTo(isDoingNothing)

            callback.onRecordingStart()
            assertThat(lastModel).isEqualTo(isRecording)

            callback.onRecordingEnd()
            assertThat(lastModel).isEqualTo(isDoingNothing)
        }

    @Test
    fun data_whenRecording_matchesController() =
        testScope.runTest {
            whenever(controller.isRecording).thenReturn(true)
            whenever(controller.isStarting).thenReturn(false)

            val lastModel by
                collectLastValue(
                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
                )
            runCurrent()

            assertThat(lastModel).isEqualTo(isRecording)
        }

    @Test
    fun data_whenStarting_matchesController() =
        testScope.runTest {
            whenever(controller.isRecording).thenReturn(false)
            whenever(controller.isStarting).thenReturn(true)

            val lastModel by
                collectLastValue(
                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
                )
            runCurrent()

            assertThat(lastModel).isEqualTo(isStarting0)
        }

    @Test
    fun data_whenRecordingAndStarting_matchesControllerRecording() =
        testScope.runTest {
            whenever(controller.isRecording).thenReturn(true)
            whenever(controller.isStarting).thenReturn(true)

            val lastModel by
                collectLastValue(
                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
                )
            runCurrent()

            assertThat(lastModel).isEqualTo(isRecording)
        }

    private companion object {
        val TEST_USER = UserHandle.of(1)!!
    }
}
+163 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor

import android.app.Dialog
import android.os.UserHandle
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.dialogTransitionAnimator
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.plugins.activityStarter
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
import com.android.systemui.qs.tiles.impl.screenrecord.domain.model.ScreenRecordTileModel
import com.android.systemui.screenrecord.RecordingController
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class ScreenRecordTileUserActionInteractorTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private val keyguardInteractor = kosmos.keyguardInteractor
    private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
    private val featureFlags = kosmos.featureFlagsClassic
    private val activityStarter = kosmos.activityStarter
    private val keyguardDismissUtil = mock<KeyguardDismissUtil>()
    private val panelInteractor = mock<PanelInteractor>()
    private val dialog = mock<Dialog>()
    private val recordingController =
        mock<RecordingController> {
            whenever(
                    createScreenRecordDialog(
                        eq(context),
                        eq(featureFlags),
                        eq(dialogTransitionAnimator),
                        eq(activityStarter),
                        any()
                    )
                )
                .thenReturn(dialog)
        }

    private val underTest =
        ScreenRecordTileUserActionInteractor(
            context,
            testScope.testScheduler,
            testScope.testScheduler,
            recordingController,
            keyguardInteractor,
            keyguardDismissUtil,
            dialogTransitionAnimator,
            panelInteractor,
            mock<MediaProjectionMetricsLogger>(),
            featureFlags,
            activityStarter,
        )

    @Test
    fun handleClick_whenStarting_cancelCountdown() = runTest {
        val startingModel = ScreenRecordTileModel.Starting(0)

        underTest.handleInput(QSTileInputTestKtx.click(startingModel))

        verify(recordingController).cancelCountdown()
    }

    @Test
    fun handleClick_whenRecording_stopRecording() = runTest {
        val recordingModel = ScreenRecordTileModel.Recording

        underTest.handleInput(QSTileInputTestKtx.click(recordingModel))

        verify(recordingController).stopRecording()
    }

    @Test
    fun handleClick_whenDoingNothing_createDialogDismissPanelShowDialog() = runTest {
        val recordingModel = ScreenRecordTileModel.DoingNothing

        underTest.handleInput(QSTileInputTestKtx.click(recordingModel))
        val onStartRecordingClickedCaptor = argumentCaptor<Runnable>()
        verify(recordingController)
            .createScreenRecordDialog(
                eq(context),
                eq(featureFlags),
                eq(dialogTransitionAnimator),
                eq(activityStarter),
                onStartRecordingClickedCaptor.capture()
            )

        val onDismissActionCaptor = argumentCaptor<OnDismissAction>()
        verify(keyguardDismissUtil)
            .executeWhenUnlocked(onDismissActionCaptor.capture(), eq(false), eq(true))
        onDismissActionCaptor.value.onDismiss()
        verify(dialog).show() // because the view was null

        // When starting the recording, we collapse the shade and disable the dialog animation.
        onStartRecordingClickedCaptor.value.run()
        verify(dialogTransitionAnimator).disableAllCurrentDialogsExitAnimations()
        verify(panelInteractor).collapsePanels()
    }

    /**
     * When the input view is not null and keyguard is not showing, dialog should animate and show
     */
    @Test
    fun handleClickFromView_whenDoingNothing_whenKeyguardNotShowing_showDialogFromView() = runTest {
        val view = mock<View>()
        kosmos.fakeKeyguardRepository.setKeyguardShowing(false)

        val recordingModel = ScreenRecordTileModel.DoingNothing

        underTest.handleInput(QSTileInputTestKtx.click(recordingModel, UserHandle.CURRENT, view))
        val onStartRecordingClickedCaptor = argumentCaptor<Runnable>()
        verify(recordingController)
            .createScreenRecordDialog(
                eq(context),
                eq(featureFlags),
                eq(dialogTransitionAnimator),
                eq(activityStarter),
                onStartRecordingClickedCaptor.capture()
            )

        val onDismissActionCaptor = argumentCaptor<OnDismissAction>()
        verify(keyguardDismissUtil)
            .executeWhenUnlocked(onDismissActionCaptor.capture(), eq(false), eq(true))
        onDismissActionCaptor.value.onDismiss()
        verify(dialogTransitionAnimator).showFromView(eq(dialog), eq(view), any(), eq(true))
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.tiles.impl.screenrecord.ui

import android.graphics.drawable.TestStubDrawable
import android.text.TextUtils
import android.widget.Switch
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
import com.android.systemui.qs.tiles.impl.screenrecord.domain.model.ScreenRecordTileModel
import com.android.systemui.qs.tiles.impl.screenrecord.domain.ui.ScreenRecordTileMapper
import com.android.systemui.qs.tiles.impl.screenrecord.qsScreenRecordTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class ScreenRecordTileMapperTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val config = kosmos.qsScreenRecordTileConfig

    private lateinit var mapper: ScreenRecordTileMapper

    @Before
    fun setup() {
        mapper =
            ScreenRecordTileMapper(
                context.orCreateTestableResources
                    .apply {
                        addOverride(R.drawable.qs_screen_record_icon_on, TestStubDrawable())
                        addOverride(R.drawable.qs_screen_record_icon_off, TestStubDrawable())
                    }
                    .resources,
                context.theme
            )
    }

    @Test
    fun activeStateMatchesRecordingDataModel() {
        val inputModel = ScreenRecordTileModel.Recording

        val outputState = mapper.map(config, inputModel)

        val expectedState =
            createScreenRecordTileState(
                QSTileState.ActivationState.ACTIVE,
                R.drawable.qs_screen_record_icon_on,
                context.getString(R.string.quick_settings_screen_record_stop),
            )
        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
    }

    @Test
    fun activeStateMatchesStartingDataModel() {
        val timeLeft = 0L
        val inputModel = ScreenRecordTileModel.Starting(timeLeft)

        val outputState = mapper.map(config, inputModel)

        val expectedState =
            createScreenRecordTileState(
                QSTileState.ActivationState.ACTIVE,
                R.drawable.qs_screen_record_icon_on,
                String.format("%d...", timeLeft)
            )
        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
    }

    @Test
    fun inactiveStateMatchesDisabledDataModel() {
        val inputModel = ScreenRecordTileModel.DoingNothing

        val outputState = mapper.map(config, inputModel)

        val expectedState =
            createScreenRecordTileState(
                QSTileState.ActivationState.INACTIVE,
                R.drawable.qs_screen_record_icon_off,
                context.getString(R.string.quick_settings_screen_record_start),
            )
        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
    }

    private fun createScreenRecordTileState(
        activationState: QSTileState.ActivationState,
        iconRes: Int,
        secondaryLabel: String,
    ): QSTileState {
        val label = context.getString(R.string.quick_settings_screen_record_label)

        return QSTileState(
            { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
            label,
            activationState,
            secondaryLabel,
            setOf(QSTileState.UserAction.CLICK),
            if (TextUtils.isEmpty(secondaryLabel)) label
            else TextUtils.concat(label, ", ", secondaryLabel),
            null,
            if (activationState == QSTileState.ActivationState.INACTIVE)
                QSTileState.SideViewIcon.Chevron
            else QSTileState.SideViewIcon.None,
            QSTileState.EnabledState.ENABLED,
            Switch::class.qualifiedName
        )
    }
}
+86 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor

import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.screenrecord.domain.model.ScreenRecordTileModel
import com.android.systemui.screenrecord.RecordingController
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart

/** Observes screen record state changes providing the [ScreenRecordTileModel]. */
class ScreenRecordTileDataInteractor
@Inject
constructor(
    @Background private val bgCoroutineContext: CoroutineContext,
    private val recordingController: RecordingController,
) : QSTileDataInteractor<ScreenRecordTileModel> {

    override fun tileData(
        user: UserHandle,
        triggers: Flow<DataUpdateTrigger>
    ): Flow<ScreenRecordTileModel> =
        ConflatedCallbackFlow.conflatedCallbackFlow {
                val callback =
                    object : RecordingController.RecordingStateChangeCallback {
                        override fun onRecordingStart() {
                            trySend(ScreenRecordTileModel.Recording)
                        }
                        override fun onRecordingEnd() {
                            trySend(ScreenRecordTileModel.DoingNothing)
                        }
                        override fun onCountdown(millisUntilFinished: Long) {
                            trySend(ScreenRecordTileModel.Starting(millisUntilFinished))
                        }
                        override fun onCountdownEnd() {
                            if (
                                !recordingController.isRecording && !recordingController.isStarting
                            ) {
                                // The tile was in Starting state and got canceled before recording
                                trySend(ScreenRecordTileModel.DoingNothing)
                            }
                        }
                    }
                recordingController.addCallback(callback)
                awaitClose { recordingController.removeCallback(callback) }
            }
            .onStart { emit(generateModel()) }
            .distinctUntilChanged()
            .flowOn(bgCoroutineContext)

    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)

    private fun generateModel(): ScreenRecordTileModel {
        if (recordingController.isRecording) {
            return ScreenRecordTileModel.Recording
        } else if (recordingController.isStarting) {
            return ScreenRecordTileModel.Starting(0)
        } else {
            return ScreenRecordTileModel.DoingNothing
        }
    }
}
+135 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading