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

Commit bad11c8a authored by Lucas Silva's avatar Lucas Silva
Browse files

Update HomeControlsDreamService to support HSUM

The service will always run in the foreground user, and will no longer
depend on the interactor directly. Instead, it will use a data source -
which will be a remote data source when the flag is enabled.

Bug: 370691405
Test: atest HomeControlsDreamServiceTest
Flag: com.android.systemui.home_controls_dream_hsum
Change-Id: I263379251895e8dacbd668dc4c51d445c9514620
parent 0b440fd9
Loading
Loading
Loading
Loading
+63 −35
Original line number Diff line number Diff line
@@ -16,25 +16,37 @@
package com.android.systemui.dreams.homecontrols

import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.os.powerManager
import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL
import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_DREAM
import android.service.controls.ControlsProviderService.EXTRA_CONTROLS_SURFACE
import android.service.dreams.DreamService
import android.window.TaskFragmentInfo
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent
import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
import com.android.systemui.dreams.homecontrols.shared.model.fakeHomeControlsDataSource
import com.android.systemui.dreams.homecontrols.shared.model.homeControlsDataSource
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.android.systemui.util.wakelock.WakeLockFake
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,7 +59,6 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -62,13 +73,18 @@ class HomeControlsDreamServiceTest : SysuiTestCase() {
        WakeLockFake.Builder(context).apply { setWakeLock(fakeWakeLock) }
    }

    private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher)

    private val taskFragmentComponent = mock<TaskFragmentComponent>()
    private val activity = mock<Activity>()
    private val onCreateCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>()
    private val onInfoChangedCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>()
    private val hideCallback = argumentCaptor<() -> Unit>()
    private val dreamServiceDelegate =
        mock<DreamServiceDelegate> { on { getActivity(any()) } doReturn activity }
    private var dreamService =
        mock<DreamService> {
            on { activity } doReturn activity
            on { redirectWake } doReturn false
        }

    private val taskFragmentComponentFactory =
        mock<TaskFragmentComponent.Factory> {
@@ -82,12 +98,32 @@ class HomeControlsDreamServiceTest : SysuiTestCase() {
            } doReturn taskFragmentComponent
        }

    private val underTest: HomeControlsDreamService by lazy { buildService() }
    private val underTest: HomeControlsDreamServiceImpl by lazy {
        with(kosmos) {
            HomeControlsDreamServiceImpl(
                taskFragmentFactory = taskFragmentComponentFactory,
                wakeLockBuilder = fakeWakeLockBuilder,
                powerManager = powerManager,
                systemClock = fakeSystemClock,
                dataSource = homeControlsDataSource,
                logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest"),
                service = dreamService,
                lifecycleOwner = lifecycleOwner,
            )
        }
    }

    @Before
    fun setup() {
        whenever(kosmos.controlsComponent.getControlsListingController())
            .thenReturn(Optional.of(kosmos.controlsListingController))
        Dispatchers.setMain(kosmos.testDispatcher)
        kosmos.fakeHomeControlsDataSource.setComponentInfo(
            HomeControlsComponentInfo(PANEL_COMPONENT, true)
        )
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
@@ -108,13 +144,10 @@ class HomeControlsDreamServiceTest : SysuiTestCase() {
    @Test
    fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() =
        testScope.runTest {
            val serviceWithNullActivity =
                buildService(
                    mock<DreamServiceDelegate> { on { getActivity(underTest) } doReturn null }
                )

            serviceWithNullActivity.onAttachedToWindow()
            dreamService = mock<DreamService> { on { activity } doReturn null }
            underTest.onAttachedToWindow()
            verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
            verify(dreamService).finish()
        }

    @Test
@@ -137,9 +170,9 @@ class HomeControlsDreamServiceTest : SysuiTestCase() {
    @Test
    fun testFinishesDreamWithoutRestartingActivityWhenNotRedirectingWakes() =
        testScope.runTest {
            whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(false)
            underTest.onAttachedToWindow()
            onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>())
            runCurrent()
            verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())

            // Task fragment becomes empty
@@ -149,16 +182,21 @@ class HomeControlsDreamServiceTest : SysuiTestCase() {
            advanceUntilIdle()
            // Dream is finished and activity is not restarted
            verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
            verify(dreamServiceDelegate, never()).wakeUp(any())
            verify(dreamServiceDelegate).finish(any())
            verify(dreamService, never()).wakeUp()
            verify(dreamService).finish()
        }

    @Test
    fun testRestartsActivityWhenRedirectingWakes() =
        testScope.runTest {
            whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(true)
            dreamService =
                mock<DreamService> {
                    on { activity } doReturn activity
                    on { redirectWake } doReturn true
                }
            underTest.onAttachedToWindow()
            onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>())
            runCurrent()
            verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())

            // Task fragment becomes empty
@@ -166,30 +204,20 @@ class HomeControlsDreamServiceTest : SysuiTestCase() {
                mock<TaskFragmentInfo> { on { isEmpty } doReturn true }
            )
            advanceUntilIdle()

            // Activity is restarted instead of finishing the dream.
            verify(taskFragmentComponent, times(2)).startActivityInTaskFragment(intentMatcher())
            verify(dreamServiceDelegate).wakeUp(any())
            verify(dreamServiceDelegate, never()).finish(any())
            verify(dreamService).wakeUp()
            verify(dreamService, never()).finish()
        }

    private fun intentMatcher() =
        argThat<Intent> {
            getIntExtra(EXTRA_CONTROLS_SURFACE, CONTROLS_SURFACE_ACTIVITY_PANEL) ==
                CONTROLS_SURFACE_DREAM
                CONTROLS_SURFACE_DREAM && component == PANEL_COMPONENT
        }

    private fun buildService(
        activityProvider: DreamServiceDelegate = dreamServiceDelegate
    ): HomeControlsDreamService =
        with(kosmos) {
            return HomeControlsDreamService(
                controlsSettingsRepository = FakeControlsSettingsRepository(),
                taskFragmentFactory = taskFragmentComponentFactory,
                homeControlsComponentInteractor = homeControlsComponentInteractor,
                wakeLockBuilder = fakeWakeLockBuilder,
                dreamServiceDelegate = activityProvider,
                bgDispatcher = testDispatcher,
                logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest")
            )
    private companion object {
        val PANEL_COMPONENT = ComponentName("test.pkg", "test.panel")
    }
}
+20 −14
Original line number Diff line number Diff line
@@ -33,9 +33,10 @@ import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
import com.android.systemui.dreams.DreamOverlayService;
import com.android.systemui.dreams.SystemDialogsCloser;
import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
import com.android.systemui.dreams.homecontrols.DreamServiceDelegate;
import com.android.systemui.dreams.homecontrols.DreamServiceDelegateImpl;
import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
import com.android.systemui.dreams.homecontrols.dagger.HomeControlsDataSourceModule;
import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent;
import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.pipeline.shared.TileSpec;
import com.android.systemui.qs.shared.model.TileCategory;
@@ -63,11 +64,13 @@ import javax.inject.Named;
@Module(includes = {
        RegisteredComplicationsModule.class,
        LowLightDreamModule.class,
            ScrimModule.class
        ScrimModule.class,
        HomeControlsDataSourceModule.class,
},
        subcomponents = {
                ComplicationComponent.class,
                DreamOverlayComponent.class,
                HomeControlsRemoteServiceComponent.class,
        })
public interface DreamModule {
    String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
@@ -112,6 +115,15 @@ public interface DreamModule {
    Service bindHomeControlsDreamService(
            HomeControlsDreamService service);

    /**
     * Provides Home Controls Remote Service
     */
    @Binds
    @IntoMap
    @ClassKey(HomeControlsRemoteService.class)
    Service bindHomeControlsRemoteService(
            HomeControlsRemoteService service);

    /**
     * Provides a touch inset manager for dreams.
     */
@@ -202,10 +214,4 @@ public interface DreamModule {
                QSTilePolicy.NoRestrictions.INSTANCE
                );
    }


    /** Provides delegate to allow for testing of dream service */
    @Binds
    DreamServiceDelegate bindDreamDelegate(DreamServiceDelegateImpl impl);

}
+0 −34
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.dreams.homecontrols

import android.app.Activity
import android.service.dreams.DreamService

/** Provides abstraction for [DreamService] methods, so they can be mocked in tests. */
interface DreamServiceDelegate {
    /** Wrapper for [DreamService.getActivity] which can be mocked in tests. */
    fun getActivity(dreamService: DreamService): Activity?

    /** Wrapper for [DreamService.wakeUp] which can be mocked in tests. */
    fun wakeUp(dreamService: DreamService)

    /** Wrapper for [DreamService.finish] which can be mocked in tests. */
    fun finish(dreamService: DreamService)

    /** Wrapper for [DreamService.getRedirectWake] which can be mocked in tests. */
    fun redirectWake(dreamService: DreamService): Boolean
}
+0 −38
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.dreams.homecontrols

import android.app.Activity
import android.service.dreams.DreamService
import javax.inject.Inject

class DreamServiceDelegateImpl @Inject constructor() : DreamServiceDelegate {
    override fun getActivity(dreamService: DreamService): Activity {
        return dreamService.activity
    }

    override fun finish(dreamService: DreamService) {
        dreamService.finish()
    }

    override fun wakeUp(dreamService: DreamService) {
        dreamService.wakeUp()
    }

    override fun redirectWake(dreamService: DreamService): Boolean {
        return dreamService.redirectWake
    }
}
+108 −66
Original line number Diff line number Diff line
@@ -16,75 +16,109 @@

package com.android.systemui.dreams.homecontrols

import android.annotation.SuppressLint
import android.content.Intent
import android.os.PowerManager
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.coroutines.newTracingContext
import com.android.systemui.dagger.qualifiers.Background
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
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.dreams.homecontrols.service.TaskFragmentComponent
import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.dagger.DreamLog
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.wakelock.WakeLock
import com.android.systemui.util.wakelock.WakeLock.Builder.NO_TIMEOUT
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
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 com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

/**
 * [DreamService] which embeds the user's chosen home controls app to allow it to display as a
 * screensaver. This service will run in the foreground user context.
 */
class HomeControlsDreamService
@Inject
constructor(private val factory: HomeControlsDreamServiceImpl.Factory) :
    DreamService(), LifecycleOwner {

    private val dispatcher = ServiceLifecycleDispatcher(this)
    override val lifecycle: Lifecycle
        get() = dispatcher.lifecycle

    private val impl: HomeControlsDreamServiceImpl by lazy { factory.create(this, this) }

    override fun onCreate() {
        dispatcher.onServicePreSuperOnCreate()
        super.onCreate()
    }

    override fun onDreamingStarted() {
        dispatcher.onServicePreSuperOnStart()
        super.onDreamingStarted()
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        impl.onAttachedToWindow()
    }

    override fun onDetachedFromWindow() {
        dispatcher.onServicePreSuperOnDestroy()
        super.onDetachedFromWindow()
        impl.onDetachedFromWindow()
    }
}

/**
 * Implementation of the home controls dream service, which allows for injecting a [DreamService]
 * and [LifecycleOwner] for testing.
 */
class HomeControlsDreamServiceImpl
@AssistedInject
constructor(
    private val controlsSettingsRepository: ControlsSettingsRepository,
    private val taskFragmentFactory: TaskFragmentComponent.Factory,
    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
    private val wakeLockBuilder: WakeLock.Builder,
    private val dreamServiceDelegate: DreamServiceDelegate,
    @Background private val bgDispatcher: CoroutineDispatcher,
    @DreamLog logBuffer: LogBuffer
) : DreamService() {

    private val serviceJob = SupervisorJob()
    private val serviceScope =
        CoroutineScope(bgDispatcher + serviceJob + newTracingContext("HomeControlsDreamService"))
    private val powerManager: PowerManager,
    private val systemClock: SystemClock,
    private val dataSource: HomeControlsDataSource,
    @DreamLog logBuffer: LogBuffer,
    @Assisted private val service: DreamService,
    @Assisted lifecycleOwner: LifecycleOwner,
) : LifecycleOwner by lifecycleOwner {

    private val logger = DreamLogger(logBuffer, TAG)
    private lateinit var taskFragmentComponent: TaskFragmentComponent
    private val wakeLock: WakeLock by lazy {
        wakeLockBuilder
            .setMaxTimeout(NO_TIMEOUT)
            .setMaxTimeout(WakeLock.Builder.NO_TIMEOUT)
            .setTag(TAG)
            .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
            .build()
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        val activity = dreamServiceDelegate.getActivity(this)
    fun onAttachedToWindow() {
        val activity = service.activity
        if (activity == null) {
            finish()
            service.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(
                    activity = activity,
                    onCreateCallback = { launchActivity() },
                    onInfoChangedCallback = this::onTaskFragmentInfoChanged,
                    hide = { endDream(false) }
                    hide = { endDream(false) },
                )
                .apply { createTaskFragment() }

@@ -99,53 +133,61 @@ constructor(
    }

    private fun endDream(handleRedirect: Boolean) {
        homeControlsComponentInteractor.onDreamEndUnexpectedly()
        if (handleRedirect && dreamServiceDelegate.redirectWake(this)) {
            dreamServiceDelegate.wakeUp(this)
            serviceScope.launch {
        pokeUserActivity()
        if (handleRedirect && service.redirectWake) {
            service.wakeUp()
            lifecycleScope.launch {
                delay(ACTIVITY_RESTART_DELAY)
                launchActivity()
            }
        } else {
            dreamServiceDelegate.finish(this)
            service.finish()
        }
    }

    private fun launchActivity() {
        val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
        val componentName = homeControlsComponentInteractor.panelComponent.value
        lifecycleScope.launch {
            val (componentName, setting) = dataSource.componentInfo.first()
            logger.d("Starting embedding $componentName")
            val intent =
                Intent().apply {
                    component = componentName
                putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting)
                    putExtra(
                        ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
                        setting,
                    )
                    putExtra(
                        ControlsProviderService.EXTRA_CONTROLS_SURFACE,
                    ControlsProviderService.CONTROLS_SURFACE_DREAM
                        ControlsProviderService.CONTROLS_SURFACE_DREAM,
                    )
                }
            taskFragmentComponent.startActivityInTaskFragment(intent)
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
    fun onDetachedFromWindow() {
        wakeLock.release(TAG)
        taskFragmentComponent.destroy()
        serviceScope.launch {
            delay(CANCELLATION_DELAY_AFTER_DETACHED)
            serviceJob.cancel("Dream detached from window")
    }

    @SuppressLint("MissingPermission")
    private fun pokeUserActivity() {
        powerManager.userActivity(
            systemClock.uptimeMillis(),
            PowerManager.USER_ACTIVITY_EVENT_OTHER,
            PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS,
        )
    }

    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
    @AssistedFactory
    interface Factory {
        fun create(
            service: DreamService,
            lifecycleOwner: LifecycleOwner,
        ): HomeControlsDreamServiceImpl
    }

    companion object {
        /**
         * Defines the delay after wakeup where we should attempt to restart the embedded activity.
         * When a wakeup is redirected, the dream service may keep running. In this case, we should
@@ -153,6 +195,6 @@ constructor(
         * after the wakeup transition has played.
         */
        val ACTIVITY_RESTART_DELAY = 334.milliseconds
        const val TAG = "HomeControlsDreamService"
        private const val TAG = "HomeControlsDreamServiceImpl"
    }
}
Loading