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

Commit 34da0b8d authored by Lucas Silva's avatar Lucas Silva
Browse files

Implement retry logic for home panel dream

This change monitors when the home controls package is updated. If the
update happens within a certain interval of the dream finishing, we
correlate the dream exit to the update, and retry starting the dream
when the update finishes.

Fixes: 323220486
Test: atest HomeControlsInteractorTest
Flag: ACONFIG android.service.controls.flags.home_panel_dream STAGING
Change-Id: If98cf28a1e0ce8f91db51154ba1b1837a5606cc7
parent ae297076
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.systemui.dreams.homecontrols

import android.service.dream.dreamManager
import com.android.systemui.common.domain.interactor.packageChangeInteractor
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.panels.authorizedPanelsRepository
@@ -24,15 +26,19 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.fakeSystemClock

val Kosmos.homeControlsComponentInteractor by
    Kosmos.Fixture {
        HomeControlsComponentInteractor(
            selectedComponentRepository = selectedComponentRepository,
            controlsComponent,
            controlsComponent = controlsComponent,
            authorizedPanelsRepository = authorizedPanelsRepository,
            userRepository = fakeUserRepository,
            bgScope = applicationCoroutineScope,
            systemClock = fakeSystemClock,
            dreamManager = dreamManager,
            packageChangeInteractor = packageChangeInteractor,
        )
    }

+109 −37
Original line number Diff line number Diff line
@@ -20,37 +20,38 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.ServiceInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import android.service.dream.dreamManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.data.repository.fakePackageChangeRepository
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.controls.panels.SelectedComponentRepository
import com.android.systemui.controls.panels.authorizedPanelsRepository
import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY
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.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
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.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -59,36 +60,17 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()

    private lateinit var controlsComponent: ControlsComponent
    private lateinit var controlsListingController: ControlsListingController
    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
    private lateinit var underTest: HomeControlsComponentInteractor
    private lateinit var userRepository: FakeUserRepository
    private lateinit var selectedComponentRepository: SelectedComponentRepository

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

        controlsComponent = kosmos.controlsComponent
        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
        controlsListingController = kosmos.controlsListingController
        selectedComponentRepository = kosmos.selectedComponentRepository

        userRepository = kosmos.fakeUserRepository
        userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))

    fun setUp() =
        with(kosmos) {
            fakeSystemClock.setCurrentTimeMillis(0)
            fakeUserRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
            whenever(controlsComponent.getControlsListingController())
                .thenReturn(Optional.of(controlsListingController))

        underTest =
            HomeControlsComponentInteractor(
                selectedComponentRepository,
                controlsComponent,
                authorizedPanelsRepository,
                userRepository,
                kosmos.applicationCoroutineScope,
            )
            underTest = homeControlsComponentInteractor
        }

    @Test
@@ -181,22 +163,112 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() {
                authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
                whenever(controlsComponent.getControlsListingController())
                    .thenReturn(Optional.empty())
                userRepository.setSelectedUserInfo(PRIMARY_USER)
                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
                val actualValue by collectLastValue(underTest.panelComponent)
                assertThat(actualValue).isNull()
            }
        }

    @Test
    fun testMonitoringUpdatesAndRestart() =
        with(kosmos) {
            testScope.runTest {
                setActiveUser(PRIMARY_USER)
                authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
                whenever(controlsListingController.getCurrentServices())
                    .thenReturn(
                        listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
                    )

                val job = launch { underTest.monitorUpdatesAndRestart() }
                val panelComponent by collectLastValue(underTest.panelComponent)

                assertThat(panelComponent).isEqualTo(TEST_COMPONENT)
                verify(dreamManager, never()).startDream()

                fakeSystemClock.advanceTime(100)
                // The package update is started.
                fakePackageChangeRepository.notifyUpdateStarted(
                    TEST_PACKAGE,
                    UserHandle.of(PRIMARY_USER_ID),
                )
                fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds)
                // Task fragment becomes empty as a result of the update.
                underTest.onTaskFragmentEmpty()

                runCurrent()
                verify(dreamManager, never()).startDream()

                fakeSystemClock.advanceTime(500)
                // The package update is finished.
                fakePackageChangeRepository.notifyUpdateFinished(
                    TEST_PACKAGE,
                    UserHandle.of(PRIMARY_USER_ID),
                )

                runCurrent()
                verify(dreamManager).startDream()
                job.cancel()
            }
        }

    @Test
    fun testMonitoringUpdatesAndRestart_dreamEndsAfterDelay() =
        with(kosmos) {
            testScope.runTest {
                setActiveUser(PRIMARY_USER)
                authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
                whenever(controlsListingController.getCurrentServices())
                    .thenReturn(
                        listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
                    )

                val job = launch { underTest.monitorUpdatesAndRestart() }
                val panelComponent by collectLastValue(underTest.panelComponent)

                assertThat(panelComponent).isEqualTo(TEST_COMPONENT)
                verify(dreamManager, never()).startDream()

                fakeSystemClock.advanceTime(100)
                // The package update is started.
                fakePackageChangeRepository.notifyUpdateStarted(
                    TEST_PACKAGE,
                    UserHandle.of(PRIMARY_USER_ID),
                )
                fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds + 100)
                // Task fragment becomes empty as a result of the update.
                underTest.onTaskFragmentEmpty()

                runCurrent()
                verify(dreamManager, never()).startDream()

                fakeSystemClock.advanceTime(500)
                // The package update is finished.
                fakePackageChangeRepository.notifyUpdateFinished(
                    TEST_PACKAGE,
                    UserHandle.of(PRIMARY_USER_ID),
                )

                runCurrent()
                verify(dreamManager, never()).startDream()
                job.cancel()
            }
        }

    private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
        val listings =
            listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = hasPanelBoolean))
        val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) }
        val callback = withArgCaptor {
            verify(kosmos.controlsListingController).addCallback(capture())
        }
        callback.onServicesUpdated(listings)
    }

    private suspend fun TestScope.setActiveUser(user: UserInfo) {
        userRepository.setSelectedUserInfo(user)
        kosmos.fakeUserRepository.setSelectedUserInfo(user)
        kosmos.fakeUserTracker.set(listOf(user), 0)
        runCurrent()
    }
+43 −62
Original line number Diff line number Diff line
@@ -16,20 +16,19 @@
package com.android.systemui.dreams.homecontrols

import android.app.Activity
import android.content.ComponentName
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
import com.android.systemui.log.LogBuffer
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import java.util.Optional
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,78 +42,60 @@ import org.mockito.MockitoAnnotations
class HomeControlsDreamServiceTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
    @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
    @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
    @Mock private lateinit var activity: Activity
    private val logBuffer: LogBuffer = create()

    private lateinit var underTest: HomeControlsDreamService
    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
    private lateinit var fakeDreamActivityProvider: DreamActivityProvider
    private lateinit var controlsComponent: ControlsComponent
    private lateinit var controlsListingController: ControlsListingController

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
    fun setup() =
        with(kosmos) {
            MockitoAnnotations.initMocks(this@HomeControlsDreamServiceTest)
            whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
                .thenReturn(taskFragmentComponent)

        controlsSettingsRepository = FakeControlsSettingsRepository()
        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)

        controlsComponent = kosmos.controlsComponent
        controlsListingController = kosmos.controlsListingController

            whenever(controlsComponent.getControlsListingController())
                .thenReturn(Optional.of(controlsListingController))

        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor

        fakeDreamActivityProvider = DreamActivityProvider { activity }
        underTest =
            HomeControlsDreamService(
                controlsSettingsRepository,
                taskFragmentComponentFactory,
                homeControlsComponentInteractor,
                fakeDreamActivityProvider,
                logBuffer
            )
            underTest = buildService { activity }
        }

    @Test
    fun testOnAttachedToWindowCreatesTaskFragmentComponent() {
    fun testOnAttachedToWindowCreatesTaskFragmentComponent() =
        testScope.runTest {
            underTest.onAttachedToWindow()
            verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
        }

    @Test
    fun testOnDetachedFromWindowDestroyTaskFragmentComponent() {
    fun testOnDetachedFromWindowDestroyTaskFragmentComponent() =
        testScope.runTest {
            underTest.onAttachedToWindow()
            underTest.onDetachedFromWindow()
            verify(taskFragmentComponent).destroy()
        }

    @Test
    fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() {
        fakeDreamActivityProvider = DreamActivityProvider { null }
        underTest =
            HomeControlsDreamService(
                controlsSettingsRepository,
                taskFragmentComponentFactory,
                homeControlsComponentInteractor,
                fakeDreamActivityProvider,
                logBuffer
            )
    fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() =
        testScope.runTest {
            underTest = buildService { null }

            underTest.onAttachedToWindow()
            verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
        }

    companion object {
        private const val TEST_PACKAGE_PANEL = "pkg.panel"
        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
    private fun buildService(activityProvider: DreamActivityProvider): HomeControlsDreamService =
        with(kosmos) {
            return HomeControlsDreamService(
                controlsSettingsRepository = FakeControlsSettingsRepository(),
                taskFragmentFactory = taskFragmentComponentFactory,
                homeControlsComponentInteractor = homeControlsComponentInteractor,
                dreamActivityProvider = activityProvider,
                bgDispatcher = testDispatcher,
                logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest")
            )
        }
}
+33 −2
Original line number Diff line number Diff line
@@ -21,11 +21,20 @@ import android.service.controls.ControlsProviderService
import android.service.dreams.DreamService
import android.window.TaskFragmentInfo
import com.android.systemui.controls.settings.ControlsSettingsRepository
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dreams.DreamLogger
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.dagger.DreamLog
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class HomeControlsDreamService
@Inject
@@ -34,11 +43,13 @@ constructor(
    private val taskFragmentFactory: TaskFragmentComponent.Factory,
    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
    private val dreamActivityProvider: DreamActivityProvider,
    @Background private val bgDispatcher: CoroutineDispatcher,
    @DreamLog logBuffer: LogBuffer
) : DreamService() {
    private lateinit var taskFragmentComponent: TaskFragmentComponent

    private val serviceJob = SupervisorJob()
    private val serviceScope = CoroutineScope(bgDispatcher + serviceJob)
    private val logger = DreamLogger(logBuffer, "HomeControlsDreamService")
    private lateinit var taskFragmentComponent: TaskFragmentComponent

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
@@ -47,6 +58,11 @@ constructor(
            finish()
            return
        }

        // Start monitoring package updates to possibly restart the dream if the home controls
        // package is updated while we are dreaming.
        serviceScope.launch { homeControlsComponentInteractor.monitorUpdatesAndRestart() }

        taskFragmentComponent =
            taskFragmentFactory
                .create(
@@ -62,6 +78,7 @@ constructor(
        if (taskFragmentInfo.isEmpty) {
            logger.d("Finishing dream due to TaskFragment being empty")
            finish()
            homeControlsComponentInteractor.onTaskFragmentEmpty()
        }
    }

@@ -84,5 +101,19 @@ constructor(
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        taskFragmentComponent.destroy()
        serviceScope.launch {
            delay(CANCELLATION_DELAY_AFTER_DETACHED)
            serviceJob.cancel("Dream detached from window")
        }
    }

    private companion object {
        /**
         * Defines how long after the dream ends that we should keep monitoring for package updates
         * to attempt a restart of the dream. This should be larger than
         * [MAX_UPDATE_CORRELATION_DELAY] as it also includes the time the package update takes to
         * complete.
         */
        val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds
    }
}
+71 −0
Original line number Diff line number Diff line
@@ -16,8 +16,13 @@

package com.android.systemui.dreams.homecontrols.domain.interactor

import android.annotation.SuppressLint
import android.app.DreamManager
import android.content.ComponentName
import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.domain.interactor.PackageChangeInteractor
import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
@@ -27,15 +32,24 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.kotlin.pairwiseBy
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlin.math.abs
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -50,6 +64,9 @@ constructor(
    controlsComponent: ControlsComponent,
    authorizedPanelsRepository: AuthorizedPanelsRepository,
    userRepository: UserRepository,
    private val packageChangeInteractor: PackageChangeInteractor,
    private val systemClock: SystemClock,
    private val dreamManager: DreamManager,
    @Background private val bgScope: CoroutineScope
) {
    private val controlsListingController: ControlsListingController? =
@@ -115,8 +132,62 @@ constructor(
            }
            .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)

    private val taskFragmentFinished =
        MutableSharedFlow<Long>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    fun onTaskFragmentEmpty() {
        taskFragmentFinished.tryEmit(systemClock.currentTimeMillis())
    }

    /**
     * Monitors if the current home panel package is updated and causes the dream to finish, and
     * attempts to restart the dream in this case.
     */
    @SuppressLint("MissingPermission")
    suspend fun monitorUpdatesAndRestart() {
        taskFragmentFinished.resetReplayCache()
        panelComponent
            .flatMapLatest { component ->
                if (component == null) return@flatMapLatest emptyFlow()
                packageChangeInteractor.packageChanged(UserHandle.CURRENT, component.packageName)
            }
            .filter { it.isUpdate() }
            // Wait for an UpdatedStarted - UpdateFinished pair to ensure the update has finished.
            .pairwiseBy(::validateUpdatePair)
            .filterNotNull()
            .sample(taskFragmentFinished, ::Pair)
            .filter { (updateStarted, lastFinishedTimestamp) ->
                abs(updateStarted.timeMillis - lastFinishedTimestamp) <=
                    MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds
            }
            .collect { dreamManager.startDream() }
    }

    private data class PanelComponent(
        val componentName: ComponentName,
        val panelActivity: ComponentName,
    )

    companion object {
        /**
         * The maximum delay between a package update **starting** and the task fragment finishing
         * which causes us to correlate the package update as the cause of the task fragment
         * finishing.
         */
        val MAX_UPDATE_CORRELATION_DELAY = 500.milliseconds
    }
}

private fun PackageChangeModel.isUpdate() =
    this is PackageChangeModel.UpdateStarted || this is PackageChangeModel.UpdateFinished

private fun validateUpdatePair(
    updateStarted: PackageChangeModel,
    updateFinished: PackageChangeModel
): PackageChangeModel.UpdateStarted? =
    when {
        !updateStarted.isSamePackage(updateFinished) -> null
        updateStarted !is PackageChangeModel.UpdateStarted -> null
        updateFinished !is PackageChangeModel.UpdateFinished -> null
        else -> updateStarted
    }
Loading