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

Commit a340e055 authored by brycelee's avatar brycelee Committed by Bryce Lee
Browse files

Refactor low-light behavior.

This changelist expands the low light handling in SystemUI to handle
other behaviors in the future beyond the low-light dream. The low-light
detection and behavior management has been separated out from the
low-light clock dream. A new CoreStartable housing this new
functionality replaces the existing LowLightMonitor when the flag is
set. This change also integrates in the new settings introduced around
low-light behavior to determine which action should be taken.

Test: atest LowLightBehaviorCoreStartableTest
Bug: 408229468
Flag: android.os.low_light_dream_behavior
Change-Id: I5e32a2ec45edc3287ec2a5985fbba4908ef71b1d
parent 55fd113d
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -877,6 +877,15 @@ java_library {
    ],
}

// Targets that don't inherit framework aconfig libs (i.e., those that don't set
// `platform_apis: true`) must manually link them.
java_defaults {
    name: "systemui-non-platform-apis-defaults",
    static_libs: [
        "android.os.flags-aconfig-java",
    ],
}

android_robolectric_test {
    name: "SystemUiRoboTests",
    srcs: [
@@ -885,6 +894,7 @@ android_robolectric_test {
        ":SystemUI-tests-utils",
        ":SystemUI-tests-multivalent",
    ],
    defaults: ["systemui-non-platform-apis-defaults"],
    static_libs: [
        "RoboTestLibraries",
        "androidx.compose.runtime_runtime",
+263 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.lowlight

import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.domain.interactor.displayStateInteractor
import com.android.systemui.dreams.domain.interactor.dreamSettingsInteractorKosmos
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.FakeActivatable
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.lowlight.data.repository.lowLightRepository
import com.android.systemui.lowlight.data.repository.lowLightSettingsRepository
import com.android.systemui.lowlight.domain.interactor.lowLightInteractor
import com.android.systemui.lowlight.shared.model.LowLightDisplayBehavior
import com.android.systemui.lowlight.shell.lowLightBehaviorShellCommand
import com.android.systemui.lowlight.shell.lowLightShellCommand
import com.android.systemui.lowlightclock.LowLightLogger
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.settings.userTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(android.os.Flags.FLAG_LOW_LIGHT_DREAM_BEHAVIOR)
class LowLightBehaviorCoreStartableTest : SysuiTestCase() {
    val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private val Kosmos.logger: LowLightLogger by
        Kosmos.Fixture { LowLightLogger(logcatLogBuffer()) }

    private val Kosmos.underTest: LowLightBehaviorCoreStartable by
        Kosmos.Fixture {
            LowLightBehaviorCoreStartable(
                lowLightInteractor = lowLightInteractor,
                dreamSettingsInteractor = dreamSettingsInteractorKosmos,
                displayStateInteractor = displayStateInteractor,
                logger = logger,
                userLockedInteractor = userLockedInteractor,
                keyguardInteractor = keyguardInteractor,
                powerInteractor = powerInteractor,
                ambientLightModeMonitor = ambientLightModeMonitor,
                uiEventLogger = mock(),
                lowLightBehaviorShellCommand = lowLightBehaviorShellCommand,
                lowLightShellCommand = lowLightShellCommand,
                scope = backgroundScope,
            )
        }

    private fun Kosmos.setDisplayOn(screenOn: Boolean) {
        displayRepository.setDefaultDisplayOff(!screenOn)
    }

    private fun Kosmos.setDreamEnabled(enabled: Boolean) {
        fakeSettings.putBoolForUser(
            Settings.Secure.SCREENSAVER_ENABLED,
            enabled,
            selectedUserInteractor.getSelectedUserId(),
        )
    }

    private fun Kosmos.setUserUnlocked(unlocked: Boolean) {
        fakeUserRepository.setUserUnlocked(selectedUserInteractor.getSelectedUserId(), unlocked)
    }

    private val action = FakeActivatable()

    @Before
    fun setUp() {
        kosmos.setDisplayOn(false)
        kosmos.setUserUnlocked(true)
        kosmos.powerInteractor.setAwakeForTest()
        kosmos.fakeKeyguardRepository.setKeyguardShowing(true)

        // Activate dreams on charge by default
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsEnabledByDefault,
            true,
        )
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
            true,
        )
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
            false,
        )
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
            false,
        )

        kosmos.lowLightSettingsRepository.setLowLightDisplayBehaviorEnabled(
            kosmos.userTracker.userInfo,
            true,
        )
        kosmos.lowLightSettingsRepository.setLowLightDisplayBehavior(
            kosmos.userTracker.userInfo,
            LowLightDisplayBehavior.LOW_LIGHT_DREAM,
        )
        kosmos.lowLightRepository.addAction(LowLightDisplayBehavior.LOW_LIGHT_DREAM, action)
    }

    @Test
    fun testSetAmbientLowLightWhenInLowLight() =
        kosmos.runTest {
            underTest.start()

            // Turn on screen
            setDisplayOn(true)
            assertThat(action.activationCount).isEqualTo(0)
            setLowLightFromSensor(true)
            assertThat(action.activationCount).isEqualTo(1)
        }

    @Test
    fun testSetAmbientLowLightWhenDisabledInLowLight() =
        kosmos.runTest {
            lowLightSettingsRepository.setLowLightDisplayBehaviorEnabled(
                userTracker.userInfo,
                false,
            )
            underTest.start()

            // Turn on screen
            setDisplayOn(true)
            setLowLightFromSensor(true)
            runCurrent()
            assertThat(action.activationCount).isEqualTo(0)
        }

    @Test
    fun testExitAmbientLowLightWhenNotInLowLight() =
        kosmos.runTest {
            // Turn on screen
            setDisplayOn(true)
            setLowLightFromSensor(true)

            underTest.start()

            assertThat(action.cancellationCount).isEqualTo(0)
            assertThat(action.activationCount).isEqualTo(1)
            setLowLightFromSensor(false)
            assertThat(action.cancellationCount).isEqualTo(1)
            assertThat(action.activationCount).isEqualTo(1)
        }

    @Test
    fun testStopMonitorLowLightConditionsWhenScreenTurnsOff() =
        kosmos.runTest {
            underTest.start()

            setDisplayOn(true)
            assertThat(ambientLightModeMonitor.fake.started).isTrue()

            // Verify removing subscription when screen turns off.
            setDisplayOn(false)
            assertThat(ambientLightModeMonitor.fake.started).isFalse()
        }

    @Test
    fun testStopMonitorLowLightConditionsWhenDreamDisabled() =
        kosmos.runTest {
            underTest.start()

            setDisplayOn(true)
            setDreamEnabled(true)

            assertThat(ambientLightModeMonitor.fake.started).isTrue()

            setDreamEnabled(false)
            // Verify removing subscription when dream disabled.
            assertThat(ambientLightModeMonitor.fake.started).isFalse()
        }

    @Test
    fun testSubscribeIfScreenIsOnWhenStarting() =
        kosmos.runTest {
            setDisplayOn(true)

            underTest.start()
            assertThat(ambientLightModeMonitor.fake.started).isTrue()
        }

    @Test
    fun testSubscribeIfScreenIsOffForScreenOffBehaviorWhenStarting() =
        kosmos.runTest {
            lowLightRepository.addAction(LowLightDisplayBehavior.SCREEN_OFF, action)
            lowLightSettingsRepository.setLowLightDisplayBehavior(
                userTracker.userInfo,
                LowLightDisplayBehavior.SCREEN_OFF,
            )

            setDisplayOn(true)

            underTest.start()
            assertThat(ambientLightModeMonitor.fake.started).isTrue()
        }

    @Test
    fun testSubscribeIfDozingForScreenOffBehavior() =
        kosmos.runTest {
            lowLightRepository.addAction(LowLightDisplayBehavior.SCREEN_OFF, action)
            lowLightSettingsRepository.setLowLightDisplayBehavior(
                userTracker.userInfo,
                LowLightDisplayBehavior.SCREEN_OFF,
            )

            setDisplayOn(true)

            fakeKeyguardRepository.setDozeTransitionModel(
                DozeTransitionModel(from = DozeStateModel.UNINITIALIZED, to = DozeStateModel.DOZE)
            )

            underTest.start()
            assertThat(ambientLightModeMonitor.fake.started).isTrue()
        }

    private fun Kosmos.setLowLightFromSensor(lowlight: Boolean) {
        val lightMode =
            if (lowlight) {
                AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK
            } else {
                AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT
            }
        ambientLightModeMonitor.fake.setAmbientLightMode(lightMode)
    }
}
+110 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.lowlightclock

import android.content.ComponentName
import android.content.packageManager
import android.content.pm.PackageManager
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.dream.lowlight.LowLightDreamManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
class LowLightClockDreamActionTest : SysuiTestCase() {
    val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private val ambientLightMode: MutableStateFlow<Int> =
        MutableStateFlow(LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN)

    private val Kosmos.lowLightDreamManager: LowLightDreamManager by
        Kosmos.Fixture {
            mock<LowLightDreamManager> {
                on { setAmbientLightMode(any()) } doAnswer
                    { invocation ->
                        val mode = invocation.arguments[0] as Int
                        ambientLightMode.value = mode
                    }
            }
        }

    private var Kosmos.dreamComponent: ComponentName? by
        Kosmos.Fixture { ComponentName("test", "test.LowLightDream") }

    private val Kosmos.underTest: LowLightClockDreamAction by
        Kosmos.Fixture {
            LowLightClockDreamAction(
                packageManager = packageManager,
                lowLightDreamService = dreamComponent,
                lowLightDreamManager = { lowLightDreamManager },
            )
        }

    @Test
    fun testLowLightClockDreamAction_lowLightToggledOnEnable() =
        kosmos.runTest {
            val mode by collectLastValue(ambientLightMode)
            underTest.setEnabled(true)

            val job = testScope.backgroundScope.launch { underTest.activate() }

            assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)

            job.cancel()
            assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
        }

    @Test
    fun testLowLightClockDreamAction_dreamComponentEnabledOnce() =
        kosmos.runTest {
            val job = testScope.backgroundScope.launch { underTest.activate() }
            verify(packageManager)
                .setComponentEnabledSetting(
                    eq(dreamComponent!!),
                    eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
                    eq(PackageManager.DONT_KILL_APP),
                )
            clearInvocations(packageManager)

            job.cancel()

            testScope.backgroundScope.launch { underTest.activate() }

            verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any())
        }
}
+3 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.lowlightclock
import android.content.ComponentName
import android.content.packageManager
import android.content.res.mainResources
import android.platform.test.annotations.DisableFlags
import android.provider.Settings
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -37,6 +38,7 @@ import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.lowlight.AmbientLightModeMonitor
import com.android.systemui.lowlight.ambientLightModeMonitor
import com.android.systemui.lowlight.fake
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -61,6 +63,7 @@ import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@DisableFlags(android.os.Flags.FLAG_LOW_LIGHT_DREAM_BEHAVIOR)
class LowLightMonitorTest : SysuiTestCase() {
    val kosmos =
        testKosmos()
+4 −2
Original line number Diff line number Diff line
@@ -84,7 +84,8 @@ import com.android.systemui.keyguard.ui.composable.LockscreenContent;
import com.android.systemui.log.dagger.LogModule;
import com.android.systemui.log.dagger.MonitorLog;
import com.android.systemui.log.table.TableLogBuffer;
import com.android.systemui.lowlightclock.dagger.LowLightModule;
import com.android.systemui.lowlight.dagger.LowLightModule;
import com.android.systemui.lowlightclock.dagger.LowLightClockModule;
import com.android.systemui.mediaprojection.MediaProjectionModule;
import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule;
import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
@@ -126,7 +127,6 @@ import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.topui.TopUiController;
import com.android.systemui.statusbar.chips.StatusBarChipsModule;
import com.android.systemui.statusbar.connectivity.ConnectivityModule;
import com.android.systemui.statusbar.dagger.StatusBarModule;
@@ -159,6 +159,7 @@ import com.android.systemui.statusbar.ui.binder.StatusBarViewBinderModule;
import com.android.systemui.statusbar.window.StatusBarWindowModule;
import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
import com.android.systemui.topui.TopUiController;
import com.android.systemui.topui.TopUiModule;
import com.android.systemui.touchpad.TouchpadModule;
import com.android.systemui.tuner.dagger.TunerModule;
@@ -295,6 +296,7 @@ import javax.inject.Named;
        NoteTaskModule.class,
        WalletModule.class,
        LowLightModule.class,
        LowLightClockModule.class,
        PerDisplayRepositoriesModule.class
},
        subcomponents = {
Loading