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

Commit a9a56b26 authored by Tianfan Zhang's avatar Tianfan Zhang
Browse files

Hide AmbientCue when focused task is not matched to target task id. The

id mismatch would happen when app switch or circle-to-search is invoked.

Bug: 403422950
Flag: com.android.systemui.enable_underlay
Test: atest AmbientCueRepositoryTest
Test: atest AmbientCueInteractorTest
Test: atest AmbientCutViewModelTest
Change-Id: I157dae6a1030a8ddea33babf541fe3a8e3a53e5b
parent 0ef63aee
Loading
Loading
Loading
Loading
+70 −6
Original line number Diff line number Diff line
@@ -33,10 +33,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.AMBIENT_CUE_SURFACE
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.DEBOUNCE_DELAY_MS
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.EXTRA_ACTIVITY_ID
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.EXTRA_AUTOFILL_ID
import com.android.systemui.ambientcue.shared.model.ActionModel
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.advanceTimeBy
import com.android.systemui.kosmos.advanceUntilIdle
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.collectLastValue
@@ -46,6 +48,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shade.data.repository.fakeFocusedDisplayRepository
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.update
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -78,15 +81,19 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
        )

    @Test
    fun isRootViewAttached_whenHasActions_true() =
    fun isRootViewAttached_whenHasActionsAndNotDeactivatedAndTaskIdMatch_true() =
        kosmos.runTest {
            val actions by collectLastValue(underTest.actions)
            val isRootViewAttached by collectLastValue(underTest.isRootViewAttached)
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(allTargets)

            fakeFocusedDisplayRepository.setGlobalTask(RunningTaskInfo().apply { taskId = TASK_ID })
            advanceTimeBy(DEBOUNCE_DELAY_MS)
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(autofillTarget))
            advanceUntilIdle()

            assertThat(isRootViewAttached).isTrue()
        }

@@ -98,8 +105,51 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())

            fakeFocusedDisplayRepository.setGlobalTask(RunningTaskInfo().apply { taskId = TASK_ID })
            advanceTimeBy(DEBOUNCE_DELAY_MS)
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(invalidTarget1))
            advanceUntilIdle()

            assertThat(isRootViewAttached).isFalse()
        }

    @Test
    fun isRootViewAttached_deactivated_false() =
        kosmos.runTest {
            val actions by collectLastValue(underTest.actions)
            val isRootViewAttached by collectLastValue(underTest.isRootViewAttached)
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())
            fakeFocusedDisplayRepository.setGlobalTask(RunningTaskInfo().apply { taskId = TASK_ID })
            advanceTimeBy(DEBOUNCE_DELAY_MS)
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(autofillTarget))
            advanceUntilIdle()

            runCurrent()
            underTest.isDeactivated.update { true }
            runCurrent()

            assertThat(isRootViewAttached).isFalse()
        }

    @Test
    fun isRootViewAttached_taskIdNotMatch_false() =
        kosmos.runTest {
            val actions by collectLastValue(underTest.actions)
            val isRootViewAttached by collectLastValue(underTest.isRootViewAttached)
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(autofillTarget))
            advanceUntilIdle()

            fakeFocusedDisplayRepository.setGlobalTask(
                RunningTaskInfo().apply { taskId = TASK_ID_2 }
            )
            advanceTimeBy(DEBOUNCE_DELAY_MS)

            assertThat(isRootViewAttached).isFalse()
        }

@@ -131,9 +181,9 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
    fun globallyFocusedTaskId_whenFocusedTaskChange_taskIdUpdated() =
        kosmos.runTest {
            val globallyFocusedTaskId by collectLastValue(underTest.globallyFocusedTaskId)
            runCurrent()

            fakeFocusedDisplayRepository.setGlobalTask(RunningTaskInfo().apply { taskId = TASK_ID })
            advanceTimeBy(DEBOUNCE_DELAY_MS)

            assertThat(globallyFocusedTaskId).isEqualTo(TASK_ID)
        }
@@ -169,12 +219,28 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
            verify(activityStarter).startActivity(launchIntent, false)
        }

    @Test
    fun targetTaskId_updatedWithAction() =
        kosmos.runTest {
            val actions by collectLastValue(underTest.actions)
            val targetTaskId by collectLastValue(underTest.targetTaskId)
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(autofillTarget))

            runCurrent()
            assertThat(targetTaskId).isEqualTo(TASK_ID)
        }

    companion object {

        private const val TITLE_1 = "title 1"
        private const val TITLE_2 = "title 2"
        private const val SUBTITLE_1 = "subtitle 1"
        private const val SUBTITLE_2 = "subtitle 2"
        private const val TASK_ID = 1
        private const val TASK_ID_2 = 2
        private val validTarget =
            mock<SmartspaceTarget> {
                on { smartspaceTargetId } doReturn AMBIENT_CUE_SURFACE
@@ -190,7 +256,7 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
            }

        private val autofillId = AutofillId(2)
        private val activityId = ActivityId(1, Binder())
        private val activityId = ActivityId(TASK_ID, Binder())
        private val autofillTarget =
            mock<SmartspaceTarget> {
                on { smartspaceTargetId } doReturn AMBIENT_CUE_SURFACE
@@ -229,7 +295,5 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
            }

        private val allTargets = listOf(validTarget, invalidTarget1)

        private const val TASK_ID = 1
    }
}
+8 −8
Original line number Diff line number Diff line
@@ -37,19 +37,19 @@ class AmbientCueInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()

    @Test
    fun isRootViewAttached_setTrue_true() =
    fun isDeactivated_setTrue_true() =
        kosmos.runTest {
            val isRootViewAttached by collectLastValue(ambientCueInteractor.isRootViewAttached)
            ambientCueInteractor.setRootViewAttached(true)
            assertThat(isRootViewAttached).isTrue()
            val isDeactivated by collectLastValue(ambientCueRepository.isDeactivated)
            ambientCueInteractor.setDeactivated(true)
            assertThat(isDeactivated).isTrue()
        }

    @Test
    fun isRootViewAttached_setFalse_False() =
    fun isDeactivated_setFalse_False() =
        kosmos.runTest {
            val isRootViewAttached by collectLastValue(ambientCueInteractor.isRootViewAttached)
            ambientCueInteractor.setRootViewAttached(false)
            assertThat(isRootViewAttached).isFalse()
            val isDeactivated by collectLastValue(ambientCueRepository.isDeactivated)
            ambientCueInteractor.setDeactivated(false)
            assertThat(isDeactivated).isFalse()
        }

    @Test
+40 −25
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.ambientcue.ui.viewmodel

import android.content.Context
import android.content.applicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -24,6 +25,7 @@ import com.android.systemui.ambientcue.data.repository.ambientCueRepository
import com.android.systemui.ambientcue.data.repository.fake
import com.android.systemui.ambientcue.domain.interactor.ambientCueInteractor
import com.android.systemui.ambientcue.shared.model.ActionModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.advanceTimeBy
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
@@ -50,37 +52,42 @@ class AmbientCueViewModelTest : SysuiTestCase() {
    @Test
    fun isVisible_timesOut() =
        kosmos.runTest {
            ambientCueInteractor.setRootViewAttached(true)
            ambientCueInteractor.setImeVisible(false)
            runCurrent()
            initializeIsVisible()
            assertThat(viewModel.isVisible).isTrue()

            // Times out when there's no interaction
            advanceTimeBy(AmbientCueViewModel.AMBIENT_CUE_TIMEOUT_SEC)
            runCurrent()
            ambientCueRepository.fake.updateRootViewAttached()
            runCurrent()

            assertThat(viewModel.isVisible).isFalse()
        }

    @Test
    fun isVisible_whenExpanded_doesntTimeOut() =
        kosmos.runTest {
            ambientCueInteractor.setRootViewAttached(true)
            ambientCueInteractor.setImeVisible(false)
            runCurrent()
            initializeIsVisible()
            assertThat(viewModel.isVisible).isTrue()

            // Doesn't time out when expanded
            viewModel.expand()
            advanceTimeBy(AmbientCueViewModel.AMBIENT_CUE_TIMEOUT_SEC)
            runCurrent()
            ambientCueRepository.fake.updateRootViewAttached()
            runCurrent()

            assertThat(viewModel.isVisible).isTrue()
        }

    @Test
    fun isVisible_imeNotVisible_true() =
        kosmos.runTest {
            ambientCueInteractor.setRootViewAttached(true)
            ambientCueRepository.fake.setActions(testActions(applicationContext))
            ambientCueInteractor.setDeactivated(false)

            ambientCueInteractor.setImeVisible(false)
            ambientCueRepository.fake.updateRootViewAttached()
            runCurrent()

            assertThat(viewModel.isVisible).isTrue()
@@ -89,12 +96,11 @@ class AmbientCueViewModelTest : SysuiTestCase() {
    @Test
    fun isVisible_imeVisible_false() =
        kosmos.runTest {
            ambientCueInteractor.setRootViewAttached(true)
            ambientCueInteractor.setImeVisible(false)
            runCurrent()
            initializeIsVisible()
            assertThat(viewModel.isVisible).isTrue()

            ambientCueInteractor.setImeVisible(true)
            ambientCueRepository.fake.updateRootViewAttached()
            runCurrent()

            assertThat(viewModel.isVisible).isFalse()
@@ -103,7 +109,20 @@ class AmbientCueViewModelTest : SysuiTestCase() {
    @Test
    fun onClick_collapses() =
        kosmos.runTest {
            val testActions =
            ambientCueRepository.fake.setActions(testActions(applicationContext))
            ambientCueInteractor.setDeactivated(false)
            viewModel.expand()
            runCurrent()

            assertThat(viewModel.isExpanded).isTrue()
            val action: ActionViewModel = viewModel.actions.first()

            // UI Collapses upon clicking on an action
            action.onClick()
            assertThat(viewModel.isExpanded).isFalse()
        }

    private fun testActions(applicationContext: Context) =
        listOf(
            ActionModel(
                icon =
@@ -116,16 +135,12 @@ class AmbientCueViewModelTest : SysuiTestCase() {
                onPerformAction = {},
            )
        )
            ambientCueRepository.fake.setActions(testActions)
            ambientCueInteractor.setRootViewAttached(true)
            viewModel.expand()
            runCurrent()

            assertThat(viewModel.isExpanded).isTrue()
            val action: ActionViewModel = viewModel.actions.first()

            // UI Collapses upon clicking on an action
            action.onClick()
            assertThat(viewModel.isExpanded).isFalse()
    private fun Kosmos.initializeIsVisible() {
        ambientCueRepository.fake.setActions(testActions(applicationContext))
        ambientCueInteractor.setDeactivated(false)
        ambientCueInteractor.setImeVisible(false)
        ambientCueRepository.fake.updateRootViewAttached()
        runCurrent()
    }
}
+43 −8
Original line number Diff line number Diff line
@@ -37,10 +37,14 @@ import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
@@ -52,13 +56,16 @@ interface AmbientCueRepository {
    val actions: StateFlow<List<ActionModel>>

    /** If the root view is attached to the WindowManager. */
    val isRootViewAttached: MutableStateFlow<Boolean>
    val isRootViewAttached: StateFlow<Boolean>

    /** If IME is visible or not. */
    val isImeVisible: MutableStateFlow<Boolean>

    /** Task Id which is globally focused on display. */
    val globallyFocusedTaskId: StateFlow<Int>

    /** If the UI is deactivated, such as closed by user or not used for a long period. */
    val isDeactivated: MutableStateFlow<Boolean>
}

@SysUISingleton
@@ -93,6 +100,8 @@ constructor(
                            .flatMap { target -> target.actionChips }
                            .map { chip ->
                                val title = chip.title.toString()
                                val activityId =
                                    chip.extras?.getParcelable<ActivityId>(EXTRA_ACTIVITY_ID)
                                ActionModel(
                                    icon =
                                        chip.icon?.loadDrawable(applicationContext)
@@ -103,10 +112,6 @@ constructor(
                                    attribution = chip.subtitle.toString(),
                                    onPerformAction = {
                                        val intent = chip.intent
                                        val activityId =
                                            chip.extras?.getParcelable<ActivityId>(
                                                EXTRA_ACTIVITY_ID
                                            )
                                        val autofillId =
                                            chip.extras?.getParcelable<AutofillId>(
                                                EXTRA_AUTOFILL_ID
@@ -127,6 +132,7 @@ constructor(
                                            activityStarter.startActivity(intent, false)
                                        }
                                    },
                                    taskId = activityId?.taskId ?: INVALID_TASK_ID,
                                )
                            }
                    if (DEBUG) {
@@ -142,26 +148,54 @@ constructor(
                    session.close()
                }
            }
            .onEach { actions -> isRootViewAttached.update { actions.isNotEmpty() } }
            .onEach { actions ->
                if (actions.isNotEmpty()) {
                    isDeactivated.update { false }
                    targetTaskId.update { actions[0].taskId }
                }
            }
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = emptyList(),
            )

    override val isRootViewAttached: MutableStateFlow<Boolean> = MutableStateFlow(false)

    override val isImeVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)

    override val isDeactivated: MutableStateFlow<Boolean> = MutableStateFlow(false)

    @OptIn(FlowPreview::class)
    override val globallyFocusedTaskId: StateFlow<Int> =
        focusdDisplayRepository.globallyFocusedTask
            .map { it?.taskId ?: INVALID_TASK_ID }
            .distinctUntilChanged()
            // Filter out focused task quick change. For example, when user clicks ambient cue, the
            // click event will also be sent to NavBar, so it will cause a quick change of focused
            // task (Target App -> Launcher -> Target App).
            .debounce(DEBOUNCE_DELAY_MS)
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = INVALID_TASK_ID,
            )

    val targetTaskId: MutableStateFlow<Int> = MutableStateFlow(INVALID_TASK_ID)

    override val isRootViewAttached: StateFlow<Boolean> =
        combine(isDeactivated, globallyFocusedTaskId, actions) {
                isDeactivated,
                globallyFocusedTaskId,
                actions ->
                actions.isNotEmpty() &&
                    !isDeactivated &&
                    globallyFocusedTaskId == targetTaskId.value
            }
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    companion object {
        // Surface that PCC wants to push cards into
        @VisibleForTesting const val AMBIENT_CUE_SURFACE = "ambientcue"
@@ -171,5 +205,6 @@ constructor(
        private const val TAG = "AmbientCueRepository"
        private const val DEBUG = false
        private const val INVALID_TASK_ID = ActivityTaskManager.INVALID_TASK_ID
        const val DEBOUNCE_DELAY_MS = 100L
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -27,8 +27,8 @@ class AmbientCueInteractor @Inject constructor(private val repository: AmbientCu
    val actions: StateFlow<List<ActionModel>> = repository.actions
    val isImeVisible: StateFlow<Boolean> = repository.isImeVisible

    fun setRootViewAttached(isAttached: Boolean) {
        repository.isRootViewAttached.update { isAttached }
    fun setDeactivated(isDeactivated: Boolean) {
        repository.isDeactivated.update { isDeactivated }
    }

    fun setImeVisible(isVisible: Boolean) {
Loading