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

Commit 3d55c2e5 authored by Lucas Silva's avatar Lucas Silva
Browse files

Move lowlight forcing logic out of condition monitor

Remove the condition for overriding lowlight and instead move it into
the lowlight monitor itself.

Bug: 407633926
Test: atest LowLightMonitorTest
Flag: EXEMPT refactor
Change-Id: I617530acbc1c1e990f2a3c01a3b271ca7fef391b
parent 37ae7996
Loading
Loading
Loading
Loading
+73 −9
Original line number Diff line number Diff line
@@ -36,10 +36,13 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.shared.condition.Condition
import com.android.systemui.shared.condition.Monitor
import com.android.systemui.statusbar.commandline.commandRegistry
import com.android.systemui.testKosmos
import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asExecutor
import org.junit.Before
@@ -72,21 +75,24 @@ class LowLightMonitorTest : SysuiTestCase() {
    private val Kosmos.underTest: LowLightMonitor by
        Kosmos.Fixture {
            LowLightMonitor(
                { lowLightDreamManager },
                monitor,
                { setOf(condition) },
                dreamSettingsInteractorKosmos,
                displayStateInteractor,
                logger,
                dreamComponent,
                packageManager,
                backgroundScope,
                lowLightDreamManager = { lowLightDreamManager },
                conditionsMonitor = monitor,
                lowLightConditions = { setOf(condition) },
                dreamSettingsInteractor = dreamSettingsInteractorKosmos,
                displayStateInteractor = displayStateInteractor,
                logger = logger,
                lowLightDreamService = dreamComponent,
                packageManager = packageManager,
                scope = backgroundScope,
                commandRegistry = commandRegistry,
            )
        }

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

    private val Kosmos.printWriter: PrintWriter by Kosmos.Fixture { PrintWriter(StringWriter()) }

    private fun Kosmos.setDisplayOn(screenOn: Boolean) {
        displayRepository.setDefaultDisplayOff(!screenOn)
    }
@@ -99,6 +105,16 @@ class LowLightMonitorTest : SysuiTestCase() {
        )
    }

    private fun Kosmos.sendDebugCommand(enable: Boolean?) {
        val value: String =
            when (enable) {
                true -> "enable"
                false -> "disable"
                null -> "clear"
            }
        commandRegistry.onShellCommand(printWriter, arrayOf(LowLightMonitor.COMMAND_ROOT, value))
    }

    @Before
    fun setUp() {
        kosmos.setDisplayOn(false)
@@ -203,6 +219,54 @@ class LowLightMonitorTest : SysuiTestCase() {
            assertThat(condition.started).isFalse()
        }

    @Test
    fun testForceLowlightToTrue() =
        kosmos.runTest {
            setDisplayOn(true)
            // low-light condition not met
            condition.setValue(false)

            underTest.start()
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
            clearInvocations(lowLightDreamManager)

            // force state to true
            sendDebugCommand(true)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
            clearInvocations(lowLightDreamManager)

            // clear forced state
            sendDebugCommand(null)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
        }

    @Test
    fun testForceLowlightToFalse() =
        kosmos.runTest {
            setDisplayOn(true)
            // low-light condition is met
            condition.setValue(true)

            underTest.start()
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
            clearInvocations(lowLightDreamManager)

            // force state to false
            sendDebugCommand(false)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
            clearInvocations(lowLightDreamManager)

            // clear forced state and ensure we go back to low-light
            sendDebugCommand(null)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
        }

    private class FakeCondition(
        scope: CoroutineScope,
        initialValue: Boolean?,
+0 −115
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.text.TextUtils
import android.util.Log
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.shared.condition.Condition
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

/**
 * This condition registers for and fulfills cmd shell commands to force a device into or out of
 * low-light conditions.
 */
class ForceLowLightCondition
@Inject
constructor(@Background scope: CoroutineScope, commandRegistry: CommandRegistry) :
    Condition(scope, null, true) {
    /**
     * Default Constructor.
     *
     * @param commandRegistry command registry to register commands with.
     */
    init {
        if (DEBUG) {
            Log.d(TAG, "registering commands")
        }
        commandRegistry.registerCommand(COMMAND_ROOT) {
            object : Command {
                override fun execute(pw: PrintWriter, args: List<String>) {
                    if (args.size != 1) {
                        pw.println("no command specified")
                        help(pw)
                        return
                    }

                    val cmd = args[0]

                    if (TextUtils.equals(cmd, COMMAND_ENABLE_LOW_LIGHT)) {
                        logAndPrint(pw, "forcing low light")
                        updateCondition(true)
                    } else if (TextUtils.equals(cmd, COMMAND_DISABLE_LOW_LIGHT)) {
                        logAndPrint(pw, "forcing to not enter low light")
                        updateCondition(false)
                    } else if (TextUtils.equals(cmd, COMMAND_CLEAR_LOW_LIGHT)) {
                        logAndPrint(pw, "clearing any forced low light")
                        clearCondition()
                    } else {
                        pw.println("invalid command")
                        help(pw)
                    }
                }

                override fun help(pw: PrintWriter) {
                    pw.println("Usage: adb shell cmd statusbar low-light <cmd>")
                    pw.println("Supported commands:")
                    pw.println("  - enable")
                    pw.println("    forces device into low-light")
                    pw.println("  - disable")
                    pw.println("    forces device to not enter low-light")
                    pw.println("  - clear")
                    pw.println("    clears any previously forced state")
                }

                private fun logAndPrint(pw: PrintWriter, message: String) {
                    pw.println(message)
                    if (DEBUG) {
                        Log.d(TAG, message)
                    }
                }
            }
        }
    }

    override suspend fun start() {}

    override fun stop() {}

    override val startStrategy: Int
        get() = START_EAGERLY

    companion object {
        /** Command root */
        const val COMMAND_ROOT: String = "low-light"

        /** Command for forcing device into low light. */
        const val COMMAND_ENABLE_LOW_LIGHT: String = "enable"

        /** Command for preventing a device from entering low light. */
        const val COMMAND_DISABLE_LOW_LIGHT: String = "disable"

        /** Command for clearing previously forced low-light conditions. */
        const val COMMAND_CLEAR_LOW_LIGHT: String = "clear"

        private const val TAG = "ForceLowLightCondition"
        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
    }
}
+56 −2
Original line number Diff line number Diff line
@@ -27,22 +27,28 @@ import com.android.systemui.dreams.shared.model.WhenToDream
import com.android.systemui.lowlightclock.dagger.LowLightModule
import com.android.systemui.shared.condition.Condition
import com.android.systemui.shared.condition.Monitor
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.condition.ConditionalCoreStartable
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.Lazy
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
@@ -63,7 +69,9 @@ constructor(
    private val lowLightDreamService: ComponentName?,
    private val packageManager: PackageManager,
    @Background private val scope: CoroutineScope,
    private val commandRegistry: CommandRegistry,
) : ConditionalCoreStartable(conditionsMonitor) {

    /** Whether the screen is currently on. */
    private val isScreenOn = not(displayStateInteractor.isDefaultDisplayOff).distinctUntilChanged()

@@ -71,7 +79,16 @@ constructor(
    private val dreamEnabled: Flow<Boolean> =
        dreamSettingsInteractor.whenToDream.map { it != WhenToDream.NEVER }

    private val isLowLight = conflatedCallbackFlow {
    /** Whether lowlight state is being forced to a specific value. */
    private val isLowLightForced: StateFlow<Boolean?> =
        conflatedCallbackFlow {
                commandRegistry.registerCommand(COMMAND_ROOT) { LowLightCommand { trySend(it) } }
                awaitClose { commandRegistry.unregisterCommand(COMMAND_ROOT) }
            }
            .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = null)

    /** Whether the device is currently in a low-light environment. */
    private val isLowLightFromSensor = conflatedCallbackFlow {
        val token =
            conditionsMonitor.addSubscription(
                Monitor.Subscription.Builder { trySend(it) }
@@ -82,6 +99,11 @@ constructor(
        awaitClose { conditionsMonitor.removeSubscription(token) }
    }

    private val isLowLight: Flow<Boolean> =
        combine(isLowLightForced, isLowLightFromSensor) { forcedValue, sensorValue ->
            forcedValue ?: sensorValue
        }

    @OptIn(ExperimentalCoroutinesApi::class)
    override fun onStart() {
        scope.launch {
@@ -121,7 +143,39 @@ constructor(
        }
    }

    private class LowLightCommand(private val update: (Boolean?) -> Unit) : Command {
        override fun execute(pw: PrintWriter, args: List<String>) {
            val arg = args.getOrNull(0)
            if (arg == null || arg.lowercase() == "help") {
                help(pw)
                return
            }

            when (arg.lowercase()) {
                "enable" -> update(true)
                "disable" -> update(false)
                "clear" -> update(null)
                else -> {
                    pw.println("Invalid argument!")
                    help(pw)
                }
            }
        }

        override fun help(pw: PrintWriter) {
            pw.println("Usage: adb shell cmd statusbar low-light <cmd>")
            pw.println("Supported commands:")
            pw.println("  - enable")
            pw.println("    forces device into low-light")
            pw.println("  - disable")
            pw.println("    forces device to not enter low-light")
            pw.println("  - clear")
            pw.println("    clears any previously forced state")
        }
    }

    companion object {
        private const val TAG = "LowLightMonitor"
        const val COMMAND_ROOT: String = "low-light"
    }
}
+0 −6
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.lowlightclock.AmbientLightModeMonitor.DebounceAlgorithm
import com.android.systemui.lowlightclock.DirectBootCondition
import com.android.systemui.lowlightclock.ForceLowLightCondition
import com.android.systemui.lowlightclock.LowLightCondition
import com.android.systemui.lowlightclock.LowLightDisplayController
import com.android.systemui.lowlightclock.LowLightMonitor
@@ -43,11 +42,6 @@ import javax.inject.Named

@Module(includes = [LowLightDreamModule::class])
abstract class LowLightModule {
    @Binds
    @IntoSet
    @Named(LOW_LIGHT_PRECONDITIONS)
    abstract fun bindForceLowLightCondition(condition: ForceLowLightCondition): Condition

    @Binds
    @IntoSet
    @Named(LOW_LIGHT_PRECONDITIONS)
+0 −98
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.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.condition.Condition
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.google.common.truth.Truth
import java.io.PrintWriter
import java.util.Arrays
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq

@SmallTest
@RunWith(AndroidTestingRunner::class)
class ForceLowLightConditionTest : SysuiTestCase() {
    private val kosmos = Kosmos()

    @Mock private lateinit var commandRegistry: CommandRegistry

    @Mock private lateinit var callback: Condition.Callback

    @Mock private lateinit var printWriter: PrintWriter

    private lateinit var condition: ForceLowLightCondition
    private lateinit var command: Command

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        condition = ForceLowLightCondition(kosmos.testScope, commandRegistry)
        condition.addCallback(callback)
        val commandCaptor = argumentCaptor<() -> Command>()
        Mockito.verify(commandRegistry)
            .registerCommand(eq(ForceLowLightCondition.COMMAND_ROOT), commandCaptor.capture())
        command = commandCaptor.lastValue.invoke()
    }

    @Test
    fun testEnableLowLight() =
        kosmos.runTest {
            command.execute(
                printWriter,
                Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT),
            )
            Mockito.verify(callback).onConditionChanged(condition)
            Truth.assertThat(condition.isConditionSet).isTrue()
            Truth.assertThat(condition.isConditionMet).isTrue()
        }

    @Test
    fun testDisableLowLight() =
        kosmos.runTest {
            command.execute(printWriter, listOf(ForceLowLightCondition.COMMAND_DISABLE_LOW_LIGHT))
            Mockito.verify(callback).onConditionChanged(condition)
            Truth.assertThat(condition.isConditionSet).isTrue()
            Truth.assertThat(condition.isConditionMet).isFalse()
        }

    @Test
    fun testClearEnableLowLight() =
        kosmos.runTest {
            command.execute(printWriter, listOf(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT))
            Mockito.verify(callback).onConditionChanged(condition)
            Truth.assertThat(condition.isConditionSet).isTrue()
            Truth.assertThat(condition.isConditionMet).isTrue()
            Mockito.clearInvocations(callback)
            command.execute(printWriter, listOf(ForceLowLightCondition.COMMAND_CLEAR_LOW_LIGHT))
            Mockito.verify(callback).onConditionChanged(condition)
            Truth.assertThat(condition.isConditionSet).isFalse()
            Truth.assertThat(condition.isConditionMet).isFalse()
        }
}