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

Commit 80ba92cf authored by Darrell Shi's avatar Darrell Shi Committed by Android (Google) Code Review
Browse files

Merge "UIEvents for enter and exit the communal hub" into main

parents a5dcf9e3 f9ca2c6b
Loading
Loading
Loading
Loading
+205 −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.communal.log

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class CommunalLoggerStartableTest : SysuiTestCase() {
    @Mock private lateinit var uiEventLogger: UiEventLogger

    private lateinit var testScope: TestScope
    private lateinit var communalInteractor: CommunalInteractor
    private lateinit var underTest: CommunalLoggerStartable

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        val withDeps = CommunalInteractorFactory.create()
        testScope = withDeps.testScope
        communalInteractor = withDeps.communalInteractor

        underTest =
            CommunalLoggerStartable(
                testScope.backgroundScope,
                communalInteractor,
                uiEventLogger,
            )
        underTest.start()
    }

    @Test
    fun transitionStateLogging_enterCommunalHub() =
        testScope.runTest {
            // Transition state is default (non-communal)
            val transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.DEFAULT))
            communalInteractor.setTransitionState(transitionState)
            runCurrent()

            // Verify nothing is logged from the default state
            verify(uiEventLogger, never()).log(any())

            // Start transition to communal
            transitionState.value = transition(to = CommunalSceneKey.Communal)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START)

            // Finish transition to communal
            transitionState.value = idle(CommunalSceneKey.Communal)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH)
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
        }

    @Test
    fun transitionStateLogging_enterCommunalHub_canceled() =
        testScope.runTest {
            // Transition state is default (non-communal)
            val transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.DEFAULT))
            communalInteractor.setTransitionState(transitionState)
            runCurrent()

            // Verify nothing is logged from the default state
            verify(uiEventLogger, never()).log(any())

            // Start transition to communal
            transitionState.value = transition(to = CommunalSceneKey.Communal)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START)

            // Cancel the transition
            transitionState.value = idle(CommunalSceneKey.DEFAULT)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL)

            // Verify neither SHOWN nor GONE is logged
            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
        }

    @Test
    fun transitionStateLogging_exitCommunalHub() =
        testScope.runTest {
            // Transition state is communal
            val transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.Communal))
            communalInteractor.setTransitionState(transitionState)
            runCurrent()

            // Verify SHOWN is logged when it's the default state
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)

            // Start transition from communal
            transitionState.value = transition(from = CommunalSceneKey.Communal)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START)

            // Finish transition to communal
            transitionState.value = idle(CommunalSceneKey.DEFAULT)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH)
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
        }

    @Test
    fun transitionStateLogging_exitCommunalHub_canceled() =
        testScope.runTest {
            // Transition state is communal
            val transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.Communal))
            communalInteractor.setTransitionState(transitionState)
            runCurrent()

            // Clear the initial SHOWN event from the logger
            clearInvocations(uiEventLogger)

            // Start transition from communal
            transitionState.value = transition(from = CommunalSceneKey.Communal)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START)

            // Cancel the transition
            transitionState.value = idle(CommunalSceneKey.Communal)
            runCurrent()

            // Verify UiEvent logged
            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL)

            // Verify neither SHOWN nor GONE is logged
            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
        }

    private fun transition(
        from: CommunalSceneKey = CommunalSceneKey.DEFAULT,
        to: CommunalSceneKey = CommunalSceneKey.DEFAULT,
    ): ObservableCommunalTransitionState.Transition {
        return ObservableCommunalTransitionState.Transition(
            fromScene = from,
            toScene = to,
            progress = emptyFlow(),
            isInitiatedByUserInput = true,
            isUserInputOngoing = emptyFlow(),
        )
    }

    private fun idle(sceneKey: CommunalSceneKey): ObservableCommunalTransitionState.Idle {
        return ObservableCommunalTransitionState.Idle(sceneKey)
    }
}
+111 −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.communal.log

import com.android.internal.logging.UiEventLogger
import com.android.systemui.CoreStartable
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

/** A [CoreStartable] responsible for logging metrics for the communal hub. */
@SysUISingleton
class CommunalLoggerStartable
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    private val communalInteractor: CommunalInteractor,
    private val uiEventLogger: UiEventLogger,
) : CoreStartable {

    override fun start() {
        communalInteractor.transitionState
            .map { state ->
                when {
                    state.isOnCommunal() -> CommunalUiEvent.COMMUNAL_HUB_SHOWN
                    state.isNotOnCommunal() -> CommunalUiEvent.COMMUNAL_HUB_GONE
                    else -> null
                }
            }
            .filterNotNull()
            .distinctUntilChanged()
            // Drop the default value.
            .drop(1)
            .onEach { uiEvent -> uiEventLogger.log(uiEvent) }
            .launchIn(backgroundScope)

        communalInteractor.transitionState
            .pairwise()
            .map { (old, new) ->
                when {
                    new.isOnCommunal() && old.isSwipingToCommunal() ->
                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH
                    new.isOnCommunal() && old.isSwipingFromCommunal() ->
                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL
                    new.isNotOnCommunal() && old.isSwipingFromCommunal() ->
                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH
                    new.isNotOnCommunal() && old.isSwipingToCommunal() ->
                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL
                    new.isSwipingToCommunal() && old.isNotOnCommunal() ->
                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START
                    new.isSwipingFromCommunal() && old.isOnCommunal() ->
                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START
                    else -> null
                }
            }
            .filterNotNull()
            .distinctUntilChanged()
            .onEach { uiEvent -> uiEventLogger.log(uiEvent) }
            .launchIn(backgroundScope)
    }
}

/** Whether currently in communal scene. */
private fun ObservableCommunalTransitionState.isOnCommunal(): Boolean {
    return this is ObservableCommunalTransitionState.Idle && scene == CommunalSceneKey.Communal
}

/** Whether currently in a scene other than communal. */
private fun ObservableCommunalTransitionState.isNotOnCommunal(): Boolean {
    return this is ObservableCommunalTransitionState.Idle && scene != CommunalSceneKey.Communal
}

/** Whether currently transitioning from another scene to communal. */
private fun ObservableCommunalTransitionState.isSwipingToCommunal(): Boolean {
    return this is ObservableCommunalTransitionState.Transition &&
        toScene == CommunalSceneKey.Communal &&
        isInitiatedByUserInput
}

/** Whether currently transitioning from communal to another scene. */
private fun ObservableCommunalTransitionState.isSwipingFromCommunal(): Boolean {
    return this is ObservableCommunalTransitionState.Transition &&
        fromScene == CommunalSceneKey.Communal &&
        isInitiatedByUserInput
}
+0 −2
Original line number Diff line number Diff line
@@ -22,8 +22,6 @@ import com.android.internal.logging.UiEventLogger.UiEventEnum
/** UI events for the Communal Hub. */
enum class CommunalUiEvent(private val id: Int) : UiEventEnum {
    @UiEvent(doc = "Communal Hub is fully shown") COMMUNAL_HUB_SHOWN(1566),
    @UiEvent(doc = "Communal Hub starts entering") COMMUNAL_HUB_ENTERING(1575),
    @UiEvent(doc = "Communal Hub starts exiting") COMMUNAL_HUB_EXITING(1576),
    @UiEvent(doc = "Communal Hub is fully gone") COMMUNAL_HUB_GONE(1577),
    @UiEvent(doc = "Communal Hub times out") COMMUNAL_HUB_TIMEOUT(1578),
    @UiEvent(doc = "The visible content in the Communal Hub is fully loaded and rendered")
+6 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.systemui.accessibility.Magnification
import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.biometrics.BiometricNotificationService
import com.android.systemui.clipboardoverlay.ClipboardListener
import com.android.systemui.communal.log.CommunalLoggerStartable
import com.android.systemui.controls.dagger.StartControlsStartableModule
import com.android.systemui.dagger.qualifiers.PerUser
import com.android.systemui.dreams.AssistantAttentionMonitor
@@ -318,4 +319,9 @@ abstract class SystemUICoreStartableModule {
    @IntoMap
    @ClassKey(KeyguardDismissBinder::class)
    abstract fun bindKeyguardDismissBinder(impl: KeyguardDismissBinder): CoreStartable

    @Binds
    @IntoMap
    @ClassKey(CommunalLoggerStartable::class)
    abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable
}
+4 −1
Original line number Diff line number Diff line
@@ -35,7 +35,8 @@ object CommunalInteractorFactory {
    @JvmStatic
    fun create(
        testScope: TestScope = TestScope(),
        communalRepository: FakeCommunalRepository = FakeCommunalRepository(),
        communalRepository: FakeCommunalRepository =
            FakeCommunalRepository(testScope.backgroundScope),
        widgetRepository: FakeCommunalWidgetRepository =
            FakeCommunalWidgetRepository(testScope.backgroundScope),
        mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(),
@@ -51,6 +52,7 @@ object CommunalInteractorFactory {
                communalRepository = communalRepository,
            )
        return WithDependencies(
            testScope,
            communalRepository,
            widgetRepository,
            mediaRepository,
@@ -74,6 +76,7 @@ object CommunalInteractorFactory {
    }

    data class WithDependencies(
        val testScope: TestScope,
        val communalRepository: FakeCommunalRepository,
        val widgetRepository: FakeCommunalWidgetRepository,
        val mediaRepository: FakeCommunalMediaRepository,