Loading packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt +27 −50 Original line number Diff line number Diff line Loading @@ -32,17 +32,15 @@ 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.applicationCoroutineScope import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.lowlight.ambientLightModeMonitor import com.android.systemui.lowlight.fake import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor 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.data.repository.fakeUserRepository Loading @@ -52,8 +50,6 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test Loading Loading @@ -85,20 +81,13 @@ class LowLightMonitorTest : SysuiTestCase() { } } private val Kosmos.monitor: Monitor by Kosmos.Fixture { Monitor(testDispatcher.asExecutor()) } private val Kosmos.logger: LowLightLogger by Kosmos.Fixture { LowLightLogger(logcatLogBuffer()) } private val Kosmos.condition: FakeCondition by Kosmos.Fixture { FakeCondition(scope = applicationCoroutineScope, initialValue = false) } private val Kosmos.underTest: LowLightMonitor by Kosmos.Fixture { LowLightMonitor( lowLightDreamManager = { lowLightDreamManager }, conditionsMonitor = monitor, lowLightConditions = { setOf(condition) }, dreamSettingsInteractor = dreamSettingsInteractorKosmos, displayStateInteractor = displayStateInteractor, logger = logger, Loading @@ -109,6 +98,8 @@ class LowLightMonitorTest : SysuiTestCase() { userLockedInteractor = userLockedInteractor, keyguardInteractor = keyguardInteractor, powerInteractor = powerInteractor, ambientLightModeMonitor = ambientLightModeMonitor, uiEventLogger = mock(), ) } Loading Loading @@ -180,8 +171,7 @@ class LowLightMonitorTest : SysuiTestCase() { assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) // Set conditions to true condition.setValue(true) setLowLightFromSensor(true) // Verify setting low light when condition is true assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -197,9 +187,9 @@ class LowLightMonitorTest : SysuiTestCase() { setDisplayOn(true) // Set conditions to true then false condition.setValue(true) setLowLightFromSensor(true) assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) condition.setValue(false) setLowLightFromSensor(false) // Verify ambient light toggles back to light mode regular assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) Loading @@ -211,11 +201,11 @@ class LowLightMonitorTest : SysuiTestCase() { underTest.start() setDisplayOn(true) assertThat(condition.started).isTrue() assertThat(ambientLightModeMonitor.fake.started).isTrue() // Verify removing subscription when screen turns off. setDisplayOn(false) assertThat(condition.started).isFalse() assertThat(ambientLightModeMonitor.fake.started).isFalse() } @Test Loading @@ -226,11 +216,11 @@ class LowLightMonitorTest : SysuiTestCase() { setDisplayOn(true) setDreamEnabled(true) assertThat(condition.started).isTrue() assertThat(ambientLightModeMonitor.fake.started).isTrue() setDreamEnabled(false) // Verify removing subscription when dream disabled. assertThat(condition.started).isFalse() assertThat(ambientLightModeMonitor.fake.started).isFalse() } @Test Loading @@ -239,7 +229,7 @@ class LowLightMonitorTest : SysuiTestCase() { setDisplayOn(true) underTest.start() assertThat(condition.started).isTrue() assertThat(ambientLightModeMonitor.fake.started).isTrue() } @Test Loading @@ -248,7 +238,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) setUserUnlocked(true) condition.setValue(true) setLowLightFromSensor(true) fakeKeyguardRepository.setKeyguardShowing(true) fakeKeyguardRepository.setDreaming(false) Loading @@ -268,7 +258,7 @@ class LowLightMonitorTest : SysuiTestCase() { setUserUnlocked(true) fakeKeyguardRepository.setKeyguardShowing(false) fakeKeyguardRepository.setDreaming(true) condition.setValue(true) setLowLightFromSensor(true) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -287,7 +277,7 @@ class LowLightMonitorTest : SysuiTestCase() { setUserUnlocked(true) fakeKeyguardRepository.setKeyguardShowing(false) fakeKeyguardRepository.setDreaming(true) condition.setValue(true) setLowLightFromSensor(true) assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -306,7 +296,7 @@ class LowLightMonitorTest : SysuiTestCase() { dreamComponent = null underTest.start() assertThat(condition.started).isFalse() assertThat(ambientLightModeMonitor.fake.started).isFalse() } @Test Loading @@ -315,7 +305,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) // low-light condition not met condition.setValue(false) setLowLightFromSensor(false) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) Loading @@ -335,7 +325,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) // low-light condition is met condition.setValue(true) setLowLightFromSensor(true) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -355,7 +345,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) // low-light condition is false condition.setValue(false) setLowLightFromSensor(false) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) Loading @@ -369,26 +359,13 @@ class LowLightMonitorTest : SysuiTestCase() { assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) } private class FakeCondition( scope: CoroutineScope, initialValue: Boolean?, overriding: Boolean = false, @StartStrategy override val startStrategy: Int = START_EAGERLY, ) : Condition(scope, initialValue, overriding) { private var _started = false val started: Boolean get() = _started override suspend fun start() { _started = true } override fun stop() { _started = false } fun setValue(value: Boolean?) { value?.also { updateCondition(value) } ?: clearCondition() 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) } } packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt +45 −43 Original line number Diff line number Diff line Loading @@ -22,15 +22,52 @@ import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.util.Log import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lowlightclock.dagger.LowLightModule.Companion.LIGHT_SENSOR import com.android.systemui.util.sensors.AsyncSensorManager import java.io.PrintWriter import java.util.Optional import javax.inject.Inject import javax.inject.Named import javax.inject.Provider interface AmbientLightModeMonitor { companion object { const val AMBIENT_LIGHT_MODE_LIGHT = 0 const val AMBIENT_LIGHT_MODE_DARK = 1 const val AMBIENT_LIGHT_MODE_UNDECIDED = 2 } // Represents all ambient light modes. @Retention(AnnotationRetention.SOURCE) @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) annotation class AmbientLightMode /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */ fun interface Callback { fun onChange(@AmbientLightMode mode: Int) } /** Interface of the algorithm that transforms light sensor events to an ambient light mode. */ interface DebounceAlgorithm { // Setting Callback to nullable so mockito can verify without throwing NullPointerException. fun start(callback: Callback?) fun stop() fun onUpdateLightSensorEvent(value: Float) } /** * Start monitoring the current ambient light mode. * * @param callback callback that gets triggered when the ambient light mode changes. */ fun start(callback: Callback) /** Stop monitoring the current ambient light mode. */ fun stop() } /** * Monitors ambient light signals, applies a debouncing algorithm, and produces the current ambient * light mode. Loading @@ -39,33 +76,20 @@ import javax.inject.Provider * light mode. * @property sensorManager the sensor manager used to register sensor event updates. */ class AmbientLightModeMonitor @SysUISingleton class AmbientLightModeMonitorImpl @Inject constructor( private val algorithm: Optional<DebounceAlgorithm>, private val algorithm: Optional<AmbientLightModeMonitor.DebounceAlgorithm>, private val sensorManager: AsyncSensorManager, @Named(LIGHT_SENSOR) private val lightSensor: Optional<Provider<Sensor>>, ) : Dumpable { ) : AmbientLightModeMonitor { companion object { private const val TAG = "AmbientLightModeMonitor" private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) const val AMBIENT_LIGHT_MODE_LIGHT = 0 const val AMBIENT_LIGHT_MODE_DARK = 1 const val AMBIENT_LIGHT_MODE_UNDECIDED = 2 } // Represents all ambient light modes. @Retention(AnnotationRetention.SOURCE) @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) annotation class AmbientLightMode /** * Start monitoring the current ambient light mode. * * @param callback callback that gets triggered when the ambient light mode changes. */ fun start(callback: Callback) { override fun start(callback: AmbientLightModeMonitor.Callback) { if (DEBUG) Log.d(TAG, "start monitoring ambient light mode") if (lightSensor.isEmpty || lightSensor.get().get() == null) { Loading @@ -87,7 +111,7 @@ constructor( } /** Stop monitoring the current ambient light mode. */ fun stop() { override fun stop() { if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode") if (algorithm.isPresent) { Loading @@ -96,13 +120,6 @@ constructor( sensorManager.unregisterListener(mSensorEventListener) } override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println() pw.println("Ambient light mode monitor:") pw.println(" lightSensor=$lightSensor") pw.println() } private val mSensorEventListener: SensorEventListener = object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { Loading @@ -120,19 +137,4 @@ constructor( // Do nothing. } } /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */ fun interface Callback { fun onChange(@AmbientLightMode mode: Int) } /** Interface of the algorithm that transforms light sensor events to an ambient light mode. */ interface DebounceAlgorithm { // Setting Callback to nullable so mockito can verify without throwing NullPointerException. fun start(callback: Callback?) fun stop() fun onUpdateLightSensorEvent(value: Float) } } packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.ktdeleted 100644 → 0 +0 −65 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 com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.condition.Condition import javax.inject.Inject import kotlinx.coroutines.CoroutineScope /** Condition for monitoring when the device enters and exits lowlight mode. */ class LowLightCondition @Inject constructor( @Background scope: CoroutineScope, private val ambientLightModeMonitor: AmbientLightModeMonitor, private val uiEventLogger: UiEventLogger, ) : Condition(scope) { override suspend fun start() { ambientLightModeMonitor.start { lowLightMode: Int -> onLowLightChanged(lowLightMode) } } override fun stop() { ambientLightModeMonitor.stop() // Reset condition met to false. updateCondition(false) } override val startStrategy: Int get() = // As this condition keeps the lowlight sensor active, it should only run when // needed. START_WHEN_NEEDED private fun onLowLightChanged(lowLightMode: Int) { if (lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) { // Ignore undecided mode changes. return } val isLowLight = lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK if (isLowLight == isConditionMet) { // No change in condition, don't do anything. return } uiEventLogger.log( if (isLowLight) LowLightDockEvent.AMBIENT_LIGHT_TO_DARK else LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT ) updateCondition(isLowLight) } } packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt +28 −19 Original line number Diff line number Diff line Loading @@ -19,22 +19,19 @@ import android.content.ComponentName import android.content.pm.PackageManager import android.os.UserHandle import com.android.dream.lowlight.LowLightDreamManager import com.android.internal.logging.UiEventLogger import com.android.systemui.CoreStartable import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.SystemUser import com.android.systemui.dreams.dagger.DreamModule import com.android.systemui.dreams.domain.interactor.DreamSettingsInteractor import com.android.systemui.dreams.shared.model.WhenToDream import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.lowlightclock.dagger.LowLightModule import com.android.systemui.power.domain.interactor.PowerInteractor 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.user.domain.interactor.UserLockedInteractor import com.android.systemui.util.condition.ConditionalCoreStartable import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not Loading @@ -51,8 +48,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch Loading @@ -64,9 +63,6 @@ class LowLightMonitor @Inject constructor( private val lowLightDreamManager: Lazy<LowLightDreamManager>, @param:SystemUser private val conditionsMonitor: Monitor, @param:Named(LowLightModule.LOW_LIGHT_PRECONDITIONS) private val lowLightConditions: Lazy<Set<Condition>>, dreamSettingsInteractor: DreamSettingsInteractor, displayStateInteractor: DisplayStateInteractor, private val logger: LowLightLogger, Loading @@ -78,7 +74,9 @@ constructor( private val userLockedInteractor: UserLockedInteractor, keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, ) : ConditionalCoreStartable(conditionsMonitor) { private val ambientLightModeMonitor: AmbientLightModeMonitor, private val uiEventLogger: UiEventLogger, ) : CoreStartable { /** Whether the screen is currently on. */ private val isScreenOn = not(displayStateInteractor.isDefaultDisplayOff).distinctUntilChanged() Loading @@ -96,16 +94,27 @@ constructor( .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) } .addConditions(lowLightConditions.get()) .build() private val isLowLightFromSensor = conflatedCallbackFlow { ambientLightModeMonitor.start { lowLightMode: Int -> trySend(lowLightMode) } awaitClose { ambientLightModeMonitor.stop() } } .filterNot { it == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } .map { it == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK } .distinctUntilChanged() .onEach { isLowLight -> uiEventLogger.log( if (isLowLight) LowLightDockEvent.AMBIENT_LIGHT_TO_DARK else LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT ) awaitClose { conditionsMonitor.removeSubscription(token) } } // AmbientLightModeMonitor only supports a single callback, so ensure this is re-used // if there are multiple subscribers. .stateIn( scope, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0), initialValue = false, ) private val isLowLight: Flow<Boolean> = combine(isLowLightForced, isLowLightFromSensor) { forcedValue, sensorValue -> Loading @@ -129,7 +138,7 @@ constructor( ), ) override fun onStart() { override fun start() { scope.launch { if (lowLightDreamService != null) { // Note that the dream service is disabled by default. This prevents the dream from Loading packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt +7 −9 Original line number Diff line number Diff line Loading @@ -23,34 +23,33 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.lowlightclock.AmbientLightModeMonitor import com.android.systemui.lowlightclock.AmbientLightModeMonitor.DebounceAlgorithm import com.android.systemui.lowlightclock.LowLightCondition import com.android.systemui.lowlightclock.AmbientLightModeMonitorImpl import com.android.systemui.lowlightclock.LowLightDisplayController import com.android.systemui.lowlightclock.LowLightMonitor import com.android.systemui.res.R import com.android.systemui.shared.condition.Condition import dagger.Binds import dagger.BindsOptionalOf import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.IntoSet import javax.inject.Named @Module(includes = [LowLightDreamModule::class]) abstract class LowLightModule { @Binds @IntoSet @Named(LOW_LIGHT_PRECONDITIONS) abstract fun bindLowLightCondition(condition: LowLightCondition): Condition @BindsOptionalOf abstract fun bindsLowLightDisplayController(): LowLightDisplayController @BindsOptionalOf @Named(LIGHT_SENSOR) abstract fun bindsLightSensor(): Sensor @BindsOptionalOf abstract fun bindsDebounceAlgorithm(): DebounceAlgorithm @Binds abstract fun bindAmbientLightModeMonitor( impl: AmbientLightModeMonitorImpl ): AmbientLightModeMonitor /** Inject into LowLightMonitor. */ @Binds @IntoMap Loading @@ -64,7 +63,6 @@ abstract class LowLightModule { const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS: String = "alpha_animation_in_start_delay_millis" const val ALPHA_ANIMATION_DURATION_MILLIS: String = "alpha_animation_duration_millis" const val LOW_LIGHT_PRECONDITIONS: String = "low_light_preconditions" const val LIGHT_SENSOR: String = "low_light_monitor_light_sensor" /** Provides a [LogBuffer] for logs related to low-light features. */ Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt +27 −50 Original line number Diff line number Diff line Loading @@ -32,17 +32,15 @@ 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.applicationCoroutineScope import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.lowlight.ambientLightModeMonitor import com.android.systemui.lowlight.fake import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor 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.data.repository.fakeUserRepository Loading @@ -52,8 +50,6 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test Loading Loading @@ -85,20 +81,13 @@ class LowLightMonitorTest : SysuiTestCase() { } } private val Kosmos.monitor: Monitor by Kosmos.Fixture { Monitor(testDispatcher.asExecutor()) } private val Kosmos.logger: LowLightLogger by Kosmos.Fixture { LowLightLogger(logcatLogBuffer()) } private val Kosmos.condition: FakeCondition by Kosmos.Fixture { FakeCondition(scope = applicationCoroutineScope, initialValue = false) } private val Kosmos.underTest: LowLightMonitor by Kosmos.Fixture { LowLightMonitor( lowLightDreamManager = { lowLightDreamManager }, conditionsMonitor = monitor, lowLightConditions = { setOf(condition) }, dreamSettingsInteractor = dreamSettingsInteractorKosmos, displayStateInteractor = displayStateInteractor, logger = logger, Loading @@ -109,6 +98,8 @@ class LowLightMonitorTest : SysuiTestCase() { userLockedInteractor = userLockedInteractor, keyguardInteractor = keyguardInteractor, powerInteractor = powerInteractor, ambientLightModeMonitor = ambientLightModeMonitor, uiEventLogger = mock(), ) } Loading Loading @@ -180,8 +171,7 @@ class LowLightMonitorTest : SysuiTestCase() { assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) // Set conditions to true condition.setValue(true) setLowLightFromSensor(true) // Verify setting low light when condition is true assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -197,9 +187,9 @@ class LowLightMonitorTest : SysuiTestCase() { setDisplayOn(true) // Set conditions to true then false condition.setValue(true) setLowLightFromSensor(true) assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) condition.setValue(false) setLowLightFromSensor(false) // Verify ambient light toggles back to light mode regular assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) Loading @@ -211,11 +201,11 @@ class LowLightMonitorTest : SysuiTestCase() { underTest.start() setDisplayOn(true) assertThat(condition.started).isTrue() assertThat(ambientLightModeMonitor.fake.started).isTrue() // Verify removing subscription when screen turns off. setDisplayOn(false) assertThat(condition.started).isFalse() assertThat(ambientLightModeMonitor.fake.started).isFalse() } @Test Loading @@ -226,11 +216,11 @@ class LowLightMonitorTest : SysuiTestCase() { setDisplayOn(true) setDreamEnabled(true) assertThat(condition.started).isTrue() assertThat(ambientLightModeMonitor.fake.started).isTrue() setDreamEnabled(false) // Verify removing subscription when dream disabled. assertThat(condition.started).isFalse() assertThat(ambientLightModeMonitor.fake.started).isFalse() } @Test Loading @@ -239,7 +229,7 @@ class LowLightMonitorTest : SysuiTestCase() { setDisplayOn(true) underTest.start() assertThat(condition.started).isTrue() assertThat(ambientLightModeMonitor.fake.started).isTrue() } @Test Loading @@ -248,7 +238,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) setUserUnlocked(true) condition.setValue(true) setLowLightFromSensor(true) fakeKeyguardRepository.setKeyguardShowing(true) fakeKeyguardRepository.setDreaming(false) Loading @@ -268,7 +258,7 @@ class LowLightMonitorTest : SysuiTestCase() { setUserUnlocked(true) fakeKeyguardRepository.setKeyguardShowing(false) fakeKeyguardRepository.setDreaming(true) condition.setValue(true) setLowLightFromSensor(true) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -287,7 +277,7 @@ class LowLightMonitorTest : SysuiTestCase() { setUserUnlocked(true) fakeKeyguardRepository.setKeyguardShowing(false) fakeKeyguardRepository.setDreaming(true) condition.setValue(true) setLowLightFromSensor(true) assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -306,7 +296,7 @@ class LowLightMonitorTest : SysuiTestCase() { dreamComponent = null underTest.start() assertThat(condition.started).isFalse() assertThat(ambientLightModeMonitor.fake.started).isFalse() } @Test Loading @@ -315,7 +305,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) // low-light condition not met condition.setValue(false) setLowLightFromSensor(false) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) Loading @@ -335,7 +325,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) // low-light condition is met condition.setValue(true) setLowLightFromSensor(true) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) Loading @@ -355,7 +345,7 @@ class LowLightMonitorTest : SysuiTestCase() { val mode by collectLastValue(ambientLightMode) setDisplayOn(true) // low-light condition is false condition.setValue(false) setLowLightFromSensor(false) underTest.start() assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) Loading @@ -369,26 +359,13 @@ class LowLightMonitorTest : SysuiTestCase() { assertThat(mode).isEqualTo(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) } private class FakeCondition( scope: CoroutineScope, initialValue: Boolean?, overriding: Boolean = false, @StartStrategy override val startStrategy: Int = START_EAGERLY, ) : Condition(scope, initialValue, overriding) { private var _started = false val started: Boolean get() = _started override suspend fun start() { _started = true } override fun stop() { _started = false } fun setValue(value: Boolean?) { value?.also { updateCondition(value) } ?: clearCondition() 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) } }
packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt +45 −43 Original line number Diff line number Diff line Loading @@ -22,15 +22,52 @@ import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.util.Log import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lowlightclock.dagger.LowLightModule.Companion.LIGHT_SENSOR import com.android.systemui.util.sensors.AsyncSensorManager import java.io.PrintWriter import java.util.Optional import javax.inject.Inject import javax.inject.Named import javax.inject.Provider interface AmbientLightModeMonitor { companion object { const val AMBIENT_LIGHT_MODE_LIGHT = 0 const val AMBIENT_LIGHT_MODE_DARK = 1 const val AMBIENT_LIGHT_MODE_UNDECIDED = 2 } // Represents all ambient light modes. @Retention(AnnotationRetention.SOURCE) @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) annotation class AmbientLightMode /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */ fun interface Callback { fun onChange(@AmbientLightMode mode: Int) } /** Interface of the algorithm that transforms light sensor events to an ambient light mode. */ interface DebounceAlgorithm { // Setting Callback to nullable so mockito can verify without throwing NullPointerException. fun start(callback: Callback?) fun stop() fun onUpdateLightSensorEvent(value: Float) } /** * Start monitoring the current ambient light mode. * * @param callback callback that gets triggered when the ambient light mode changes. */ fun start(callback: Callback) /** Stop monitoring the current ambient light mode. */ fun stop() } /** * Monitors ambient light signals, applies a debouncing algorithm, and produces the current ambient * light mode. Loading @@ -39,33 +76,20 @@ import javax.inject.Provider * light mode. * @property sensorManager the sensor manager used to register sensor event updates. */ class AmbientLightModeMonitor @SysUISingleton class AmbientLightModeMonitorImpl @Inject constructor( private val algorithm: Optional<DebounceAlgorithm>, private val algorithm: Optional<AmbientLightModeMonitor.DebounceAlgorithm>, private val sensorManager: AsyncSensorManager, @Named(LIGHT_SENSOR) private val lightSensor: Optional<Provider<Sensor>>, ) : Dumpable { ) : AmbientLightModeMonitor { companion object { private const val TAG = "AmbientLightModeMonitor" private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) const val AMBIENT_LIGHT_MODE_LIGHT = 0 const val AMBIENT_LIGHT_MODE_DARK = 1 const val AMBIENT_LIGHT_MODE_UNDECIDED = 2 } // Represents all ambient light modes. @Retention(AnnotationRetention.SOURCE) @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) annotation class AmbientLightMode /** * Start monitoring the current ambient light mode. * * @param callback callback that gets triggered when the ambient light mode changes. */ fun start(callback: Callback) { override fun start(callback: AmbientLightModeMonitor.Callback) { if (DEBUG) Log.d(TAG, "start monitoring ambient light mode") if (lightSensor.isEmpty || lightSensor.get().get() == null) { Loading @@ -87,7 +111,7 @@ constructor( } /** Stop monitoring the current ambient light mode. */ fun stop() { override fun stop() { if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode") if (algorithm.isPresent) { Loading @@ -96,13 +120,6 @@ constructor( sensorManager.unregisterListener(mSensorEventListener) } override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println() pw.println("Ambient light mode monitor:") pw.println(" lightSensor=$lightSensor") pw.println() } private val mSensorEventListener: SensorEventListener = object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { Loading @@ -120,19 +137,4 @@ constructor( // Do nothing. } } /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */ fun interface Callback { fun onChange(@AmbientLightMode mode: Int) } /** Interface of the algorithm that transforms light sensor events to an ambient light mode. */ interface DebounceAlgorithm { // Setting Callback to nullable so mockito can verify without throwing NullPointerException. fun start(callback: Callback?) fun stop() fun onUpdateLightSensorEvent(value: Float) } }
packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.ktdeleted 100644 → 0 +0 −65 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 com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.condition.Condition import javax.inject.Inject import kotlinx.coroutines.CoroutineScope /** Condition for monitoring when the device enters and exits lowlight mode. */ class LowLightCondition @Inject constructor( @Background scope: CoroutineScope, private val ambientLightModeMonitor: AmbientLightModeMonitor, private val uiEventLogger: UiEventLogger, ) : Condition(scope) { override suspend fun start() { ambientLightModeMonitor.start { lowLightMode: Int -> onLowLightChanged(lowLightMode) } } override fun stop() { ambientLightModeMonitor.stop() // Reset condition met to false. updateCondition(false) } override val startStrategy: Int get() = // As this condition keeps the lowlight sensor active, it should only run when // needed. START_WHEN_NEEDED private fun onLowLightChanged(lowLightMode: Int) { if (lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) { // Ignore undecided mode changes. return } val isLowLight = lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK if (isLowLight == isConditionMet) { // No change in condition, don't do anything. return } uiEventLogger.log( if (isLowLight) LowLightDockEvent.AMBIENT_LIGHT_TO_DARK else LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT ) updateCondition(isLowLight) } }
packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt +28 −19 Original line number Diff line number Diff line Loading @@ -19,22 +19,19 @@ import android.content.ComponentName import android.content.pm.PackageManager import android.os.UserHandle import com.android.dream.lowlight.LowLightDreamManager import com.android.internal.logging.UiEventLogger import com.android.systemui.CoreStartable import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.SystemUser import com.android.systemui.dreams.dagger.DreamModule import com.android.systemui.dreams.domain.interactor.DreamSettingsInteractor import com.android.systemui.dreams.shared.model.WhenToDream import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.lowlightclock.dagger.LowLightModule import com.android.systemui.power.domain.interactor.PowerInteractor 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.user.domain.interactor.UserLockedInteractor import com.android.systemui.util.condition.ConditionalCoreStartable import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not Loading @@ -51,8 +48,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch Loading @@ -64,9 +63,6 @@ class LowLightMonitor @Inject constructor( private val lowLightDreamManager: Lazy<LowLightDreamManager>, @param:SystemUser private val conditionsMonitor: Monitor, @param:Named(LowLightModule.LOW_LIGHT_PRECONDITIONS) private val lowLightConditions: Lazy<Set<Condition>>, dreamSettingsInteractor: DreamSettingsInteractor, displayStateInteractor: DisplayStateInteractor, private val logger: LowLightLogger, Loading @@ -78,7 +74,9 @@ constructor( private val userLockedInteractor: UserLockedInteractor, keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, ) : ConditionalCoreStartable(conditionsMonitor) { private val ambientLightModeMonitor: AmbientLightModeMonitor, private val uiEventLogger: UiEventLogger, ) : CoreStartable { /** Whether the screen is currently on. */ private val isScreenOn = not(displayStateInteractor.isDefaultDisplayOff).distinctUntilChanged() Loading @@ -96,16 +94,27 @@ constructor( .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) } .addConditions(lowLightConditions.get()) .build() private val isLowLightFromSensor = conflatedCallbackFlow { ambientLightModeMonitor.start { lowLightMode: Int -> trySend(lowLightMode) } awaitClose { ambientLightModeMonitor.stop() } } .filterNot { it == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } .map { it == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK } .distinctUntilChanged() .onEach { isLowLight -> uiEventLogger.log( if (isLowLight) LowLightDockEvent.AMBIENT_LIGHT_TO_DARK else LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT ) awaitClose { conditionsMonitor.removeSubscription(token) } } // AmbientLightModeMonitor only supports a single callback, so ensure this is re-used // if there are multiple subscribers. .stateIn( scope, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0), initialValue = false, ) private val isLowLight: Flow<Boolean> = combine(isLowLightForced, isLowLightFromSensor) { forcedValue, sensorValue -> Loading @@ -129,7 +138,7 @@ constructor( ), ) override fun onStart() { override fun start() { scope.launch { if (lowLightDreamService != null) { // Note that the dream service is disabled by default. This prevents the dream from Loading
packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt +7 −9 Original line number Diff line number Diff line Loading @@ -23,34 +23,33 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.lowlightclock.AmbientLightModeMonitor import com.android.systemui.lowlightclock.AmbientLightModeMonitor.DebounceAlgorithm import com.android.systemui.lowlightclock.LowLightCondition import com.android.systemui.lowlightclock.AmbientLightModeMonitorImpl import com.android.systemui.lowlightclock.LowLightDisplayController import com.android.systemui.lowlightclock.LowLightMonitor import com.android.systemui.res.R import com.android.systemui.shared.condition.Condition import dagger.Binds import dagger.BindsOptionalOf import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.IntoSet import javax.inject.Named @Module(includes = [LowLightDreamModule::class]) abstract class LowLightModule { @Binds @IntoSet @Named(LOW_LIGHT_PRECONDITIONS) abstract fun bindLowLightCondition(condition: LowLightCondition): Condition @BindsOptionalOf abstract fun bindsLowLightDisplayController(): LowLightDisplayController @BindsOptionalOf @Named(LIGHT_SENSOR) abstract fun bindsLightSensor(): Sensor @BindsOptionalOf abstract fun bindsDebounceAlgorithm(): DebounceAlgorithm @Binds abstract fun bindAmbientLightModeMonitor( impl: AmbientLightModeMonitorImpl ): AmbientLightModeMonitor /** Inject into LowLightMonitor. */ @Binds @IntoMap Loading @@ -64,7 +63,6 @@ abstract class LowLightModule { const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS: String = "alpha_animation_in_start_delay_millis" const val ALPHA_ANIMATION_DURATION_MILLIS: String = "alpha_animation_duration_millis" const val LOW_LIGHT_PRECONDITIONS: String = "low_light_preconditions" const val LIGHT_SENSOR: String = "low_light_monitor_light_sensor" /** Provides a [LogBuffer] for logs related to low-light features. */ Loading