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

Commit c67717b5 authored by William Xiao's avatar William Xiao
Browse files

Fix empty state hub showing briefly when swiping from lock screen

If only start listening once the hub scene transition is committed, the
empty state may show very briefly when swiping open quickly. By
listening to the transition state instead of the scene state, we can
react sooner and load widgets before the hub shows up.

Bug: 411467094
Fixed: 411467094
Test: atest CommunalAppWidgetHostStartableTest
Flag: com.android.systemui.restrict_communal_app_widget_host_listening
Change-Id: I635c03683877af4d37e835f4f0dc237a783ddd36
parent d03bd271
Loading
Loading
Loading
Loading
+246 −211
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_RESTRICT_COMMUNAL_APP_WIDGET_HOST_LISTENING
import com.android.systemui.Flags.restrictCommunalAppWidgetHostListening
@@ -33,26 +34,24 @@ import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.interactor.setCommunalEnabled
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper
import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -72,9 +71,6 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
    @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
    @Mock private lateinit var communalWidgetHost: CommunalWidgetHost

    private lateinit var widgetManager: GlanceableHubWidgetManager
    private lateinit var helper: FakeGlanceableHubMultiUserHelper

    private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>

    private lateinit var communalInteractorSpy: CommunalInteractor
@@ -91,31 +87,30 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
        mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)

        widgetManager = kosmos.mockGlanceableHubWidgetManager
        helper = kosmos.fakeGlanceableHubMultiUserHelper
        appWidgetIdToRemove = MutableSharedFlow()
        whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove)
        communalInteractorSpy = spy(kosmos.communalInteractor)

        underTest =
            with(kosmos) {
                CommunalAppWidgetHostStartable(
                    { appWidgetHost },
                    { communalWidgetHost },
                    { communalInteractorSpy },
                { kosmos.communalSettingsInteractor },
                { kosmos.keyguardInteractor },
                { kosmos.fakeUserTracker },
                kosmos.applicationCoroutineScope,
                kosmos.testDispatcher,
                { widgetManager },
                helper,
                    { communalSettingsInteractor },
                    { keyguardInteractor },
                    { fakeUserTracker },
                    applicationCoroutineScope,
                    testDispatcher,
                    { mockGlanceableHubWidgetManager },
                    fakeGlanceableHubMultiUserHelper,
                )
            }
    }

    @Test
    fun editModeShowingStartsAppWidgetHost() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            setCommunalAvailable(false)
            communalInteractor.setEditModeOpen(true)
            verify(appWidgetHost, never()).startListening()
@@ -131,13 +126,11 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe

            verify(appWidgetHost).stopListening()
        }
        }

    @Test
    @DisableFlags(FLAG_RESTRICT_COMMUNAL_APP_WIDGET_HOST_LISTENING)
    fun communalAvailableStartsAppWidgetHost() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            setCommunalAvailable(true)
            communalInteractor.setEditModeOpen(false)
            verify(appWidgetHost, never()).startListening()
@@ -153,7 +146,6 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe

            verify(appWidgetHost).stopListening()
        }
        }

    @Test
    @EnableFlags(FLAG_RESTRICT_COMMUNAL_APP_WIDGET_HOST_LISTENING)
@@ -182,10 +174,73 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            verify(appWidgetHost).stopListening()
        }

    // Verifies that the widget host starts listening as soon as the hub transition starts.
    @Test
    @EnableFlags(FLAG_RESTRICT_COMMUNAL_APP_WIDGET_HOST_LISTENING)
    fun communalVisibleStartsAppWidgetHost() =
        kosmos.runTest {
            setCommunalAvailable(true)
            communalInteractor.setEditModeOpen(false)

            verify(appWidgetHost, never()).startListening()

            underTest.start()
            runCurrent()

            val transitionState =
                MutableStateFlow<ObservableTransitionState>(
                    ObservableTransitionState.Idle(currentScene = CommunalScenes.Blank)
                )
            communalSceneInteractor.setTransitionState(transitionState)
            runCurrent()

            // Listening has not started or stopped yet.
            verify(appWidgetHost, never()).startListening()
            verify(appWidgetHost, never()).stopListening()

            // Start transitioning to communal.
            transitionState.value =
                ObservableTransitionState.Transition(
                    fromScene = CommunalScenes.Blank,
                    toScene = CommunalScenes.Communal,
                    currentScene = flowOf(CommunalScenes.Blank),
                    progress = flowOf(0.1f),
                    isInitiatedByUserInput = false,
                    isUserInputOngoing = flowOf(false),
                )
            runCurrent()

            // Listening starts.
            verify(appWidgetHost).startListening()
            verify(appWidgetHost, never()).stopListening()

            // Start transitioning away from communal.
            transitionState.value =
                ObservableTransitionState.Transition(
                    fromScene = CommunalScenes.Communal,
                    toScene = CommunalScenes.Blank,
                    currentScene = flowOf(CommunalScenes.Communal),
                    progress = flowOf(0.1f),
                    isInitiatedByUserInput = false,
                    isUserInputOngoing = flowOf(false),
                )
            runCurrent()

            // Listening continues
            verify(appWidgetHost, never()).stopListening()

            // Finish transitioning away from communal.
            transitionState.value =
                ObservableTransitionState.Idle(currentScene = CommunalScenes.Blank)
            runCurrent()

            // Listening stops.
            verify(appWidgetHost).stopListening()
        }

    @Test
    fun communalAndEditModeNotShowingNeverStartListening() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            setCommunalAvailable(false)
            communalInteractor.setEditModeOpen(false)

@@ -195,12 +250,10 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            verify(appWidgetHost, never()).startListening()
            verify(appWidgetHost, never()).stopListening()
        }
        }

    @Test
    fun observeHostWhenCommunalIsAvailable() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            setCommunalAvailable(true)
            if (restrictCommunalAppWidgetHostListening()) {
                communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
@@ -223,12 +276,10 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe

            verify(communalWidgetHost).stopObservingHost()
        }
        }

    @Test
    fun removeAppWidgetReportedByHost() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            // Set up communal widgets
            fakeCommunalWidgetRepository.addWidget(appWidgetId = 1)
            fakeCommunalWidgetRepository.addWidget(appWidgetId = 2)
@@ -237,8 +288,7 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            underTest.start()

            // Assert communal widgets has 3
                val communalWidgets by
                    collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
            val communalWidgets by collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
            assertThat(communalWidgets).hasSize(3)

            val widget1 = communalWidgets!![0]
@@ -258,12 +308,10 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            runCurrent()
            assertThat(communalWidgets).containsExactly(widget2)
        }
        }

    @Test
    fun removeWidgetsForDeletedProfile_whenCommunalIsAvailable() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            // Communal is available and work profile is configured.
            setCommunalAvailable(true)
            if (restrictCommunalAppWidgetHostListening()) {
@@ -284,8 +332,7 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            underTest.start()
            runCurrent()

                val communalWidgets by
                    collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
            val communalWidgets by collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
            assertThat(communalWidgets).hasSize(3)

            val widget1 = communalWidgets!![0]
@@ -300,10 +347,7 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            if (restrictCommunalAppWidgetHostListening()) {
                communalSceneInteractor.changeScene(CommunalScenes.Blank, "test")
            }
                kosmos.fakeUserTracker.set(
                    userInfos = listOf(MAIN_USER_INFO),
                    selectedUserIndex = 0,
                )
            kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
            runCurrent()

            // Communal becomes available.
@@ -316,21 +360,16 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            // Both work widgets are removed.
            assertThat(communalWidgets).containsExactly(widget3)
        }
        }

    @Test
    fun removeNotLockscreenWidgets_whenCommunalIsAvailable() =
        with(kosmos) {
            testScope.runTest {
        kosmos.runTest {
            // Communal is available
            setCommunalAvailable(true)
            if (restrictCommunalAppWidgetHostListening()) {
                communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
            }
                kosmos.fakeUserTracker.set(
                    userInfos = listOf(MAIN_USER_INFO),
                    selectedUserIndex = 0,
                )
            kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
            fakeCommunalWidgetRepository.addWidget(
                appWidgetId = 1,
                userId = MAIN_USER_INFO.id,
@@ -346,36 +385,32 @@ class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTe
            underTest.start()
            runCurrent()

                val communalWidgets by
                    collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
            val communalWidgets by collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
            assertThat(communalWidgets).hasSize(1)
            assertThat(communalWidgets!![0].appWidgetId).isEqualTo(2)

            verify(communalInteractorSpy).deleteWidget(1)
            verify(communalInteractorSpy).deleteWidget(3)
        }
        }

    @Test
    fun onStartHeadlessSystemUser_registerWidgetManager_whenCommunalIsAvailable() =
        with(kosmos) {
            testScope.runTest {
                helper.setIsInHeadlessSystemUser(true)
        kosmos.runTest {
            fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true)
            underTest.start()
            runCurrent()
                verify(widgetManager, never()).register()
                verify(widgetManager, never()).unregister()
            verify(mockGlanceableHubWidgetManager, never()).register()
            verify(mockGlanceableHubWidgetManager, never()).unregister()

            // Binding to the service does not require keyguard showing
            setCommunalAvailable(true, setKeyguardShowing = false)
            fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
            runCurrent()
                verify(widgetManager).register()
            verify(mockGlanceableHubWidgetManager).register()

            setCommunalAvailable(false)
            runCurrent()
                verify(widgetManager).unregister()
            }
            verify(mockGlanceableHubWidgetManager).unregister()
        }

    private fun setCommunalAvailable(available: Boolean, setKeyguardShowing: Boolean = true) =
+3 −2
Original line number Diff line number Diff line
@@ -38,7 +38,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
@@ -92,7 +91,9 @@ constructor(
        ) {
            val listenFlow =
                if (restrictCommunalAppWidgetHostListening()) {
                    communalInteractor.isCommunalShowing
                    // Listen whenever any part of the hub is visible so that widgets show up during
                    // transitions too.
                    communalInteractor.isCommunalVisible
                } else {
                    communalInteractor.isCommunalAvailable
                }