Loading packages/SystemUI/res/values/config.xml +24 −0 Original line number Diff line number Diff line Loading @@ -723,6 +723,30 @@ <!-- Timeout to idle mode duration in milliseconds. --> <integer name="config_idleModeTimeout">10000</integer> <!-- This value is used when calculating whether the device is in ambient light mode. It is light mode when the light sensor sample value exceeds above this value. --> <integer name="config_ambientLightModeThreshold">5</integer> <!-- This value is used when calculating whether the device is in ambient dark mode. It is dark mode when the light sensor sample value drops below this value. --> <integer name="config_ambientDarkModeThreshold">2</integer> <!-- This value is used when calculating whether the device is in ambient light mode. Each sample contains light sensor events from this span of time duration. --> <integer name="config_ambientLightModeSamplingSpanMillis">10000</integer> <!-- This value is used when calculating whether the device is in ambient dark mode. Each sample contains light sensor events from this span of time duration. --> <integer name="config_ambientDarkModeSamplingSpanMillis">2000</integer> <!-- This value is used when calculating whether the device is in ambient light mode. The samples are collected at this frequency. --> <integer name="config_ambientLightModeSamplingFrequencyMillis">1000</integer> <!-- This value is used when calculating whether the device is in ambient dark mode. The samples are collected at this frequency. --> <integer name="config_ambientDarkModeSamplingFrequencyMillis">500</integer> <!-- The maximum number of attempts to reconnect to the communal source target after failing to connect --> <integer name="config_communalSourceMaxReconnectAttempts">10</integer> Loading packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java +12 −0 Original line number Diff line number Diff line Loading @@ -20,10 +20,13 @@ import android.content.Context; import android.view.View; import android.widget.FrameLayout; import com.android.systemui.idle.AmbientLightModeMonitor; import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm; import com.android.systemui.idle.dagger.IdleViewComponent; import javax.inject.Named; import dagger.Binds; import dagger.Module; import dagger.Provides; Loading @@ -44,4 +47,13 @@ public interface CommunalModule { FrameLayout view = new FrameLayout(context); return view; } /** * Provides LightSensorEventsDebounceAlgorithm as an instance to DebounceAlgorithm interface. * @param algorithm the instance of algorithm that is bound to the interface. * @return the interface that is bound to. */ @Binds AmbientLightModeMonitor.DebounceAlgorithm ambientLightDebounceAlgorithm( LightSensorEventsDebounceAlgorithm algorithm); } packages/SystemUI/src/com/android/systemui/idle/AmbientLightModeMonitor.kt +18 −34 Original line number Diff line number Diff line Loading @@ -24,17 +24,17 @@ import android.hardware.SensorManager import android.util.Log import com.android.systemui.util.sensors.AsyncSensorManager import javax.inject.Inject import kotlin.properties.Delegates /** * Monitors ambient light signals, applies a debouncing algorithm, and produces the current * [AmbientLightMode]. * * For debouncer behavior, refer to go/titan-light-sensor-debouncer. * ambient light mode. * * @property algorithm the debounce algorithm which transforms light sensor events into an * ambient light mode. * @property sensorManager the sensor manager used to register sensor event updates. */ class AmbientLightModeMonitor @Inject constructor( private val algorithm: DebounceAlgorithm, private val sensorManager: AsyncSensorManager ) { companion object { Loading @@ -49,39 +49,20 @@ class AmbientLightModeMonitor @Inject constructor( // Light sensor used to detect ambient lighting conditions. private val lightSensor: Sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) // Registered callback, which gets triggered when the ambient light mode changes. private var callback: Callback? = null // Represents all ambient light modes. @Retention(AnnotationRetention.SOURCE) @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) annotation class AmbientLightMode // The current ambient light mode. @AmbientLightMode private var mode: Int by Delegates.observable(AMBIENT_LIGHT_MODE_UNDECIDED ) { _, old, new -> if (old != new) { callback?.onChange(new) } } /** * Start monitoring the current ambient light mode. * * @param callback callback that gets triggered when the ambient light mode changes. It also * gets triggered immediately to update the current value when this function is called. * @param callback callback that gets triggered when the ambient light mode changes. */ fun start(callback: Callback) { if (DEBUG) Log.d(TAG, "start monitoring ambient light mode") if (this.callback != null) { if (DEBUG) Log.w(TAG, "already started") return } this.callback = callback callback.onChange(mode) algorithm.start(callback) sensorManager.registerListener(mSensorEventListener, lightSensor, SensorManager.SENSOR_DELAY_NORMAL) } Loading @@ -92,12 +73,7 @@ class AmbientLightModeMonitor @Inject constructor( fun stop() { if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode") if (callback == null) { if (DEBUG) Log.w(TAG, "haven't started") return } callback = null algorithm.stop() sensorManager.unregisterListener(mSensorEventListener) } Loading @@ -108,9 +84,7 @@ class AmbientLightModeMonitor @Inject constructor( return } // TODO(b/201657509): add debouncing logic. val shouldBeLowLight = event.values[0] < 10 mode = if (shouldBeLowLight) AMBIENT_LIGHT_MODE_DARK else AMBIENT_LIGHT_MODE_LIGHT algorithm.onUpdateLightSensorEvent(event.values[0]) } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { Loading @@ -124,4 +98,14 @@ class AmbientLightModeMonitor @Inject constructor( 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) } } No newline at end of file packages/SystemUI/src/com/android/systemui/idle/LightSensorEventsDebounceAlgorithm.kt 0 → 100644 +284 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.idle import android.content.res.Resources import android.util.Log import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject /** * An algorithm that receives light sensor events, debounces the signals, and transforms into an * ambient light mode: light, dark, or undecided. * * More about the algorithm at go/titan-light-sensor-debouncer. */ class LightSensorEventsDebounceAlgorithm @Inject constructor( @Main private val executor: DelayableExecutor, @Main resources: Resources ) : AmbientLightModeMonitor.DebounceAlgorithm { companion object { private const val TAG = "LightDebounceAlgorithm" private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) } // The ambient mode is considered light mode when the light sensor value increases exceeding // this value. private val lightModeThreshold = resources.getInteger(R.integer.config_ambientLightModeThreshold) // The ambient mode is considered dark mode when the light sensor value drops below this // value. private val darkModeThreshold = resources.getInteger(R.integer.config_ambientDarkModeThreshold) // Each sample for calculating light mode contains light sensor events collected for this // duration of time in milliseconds. private val lightSamplingSpanMillis = resources.getInteger(R.integer.config_ambientLightModeSamplingSpanMillis) // Each sample for calculating dark mode contains light sensor events collected for this // duration of time in milliseconds. private val darkSamplingSpanMillis = resources.getInteger(R.integer.config_ambientDarkModeSamplingSpanMillis) // The calculations for light mode is performed at this frequency in milliseconds. private val lightSamplingFrequencyMillis = resources.getInteger(R.integer.config_ambientLightModeSamplingFrequencyMillis) // The calculations for dark mode is performed at this frequency in milliseconds. private val darkSamplingFrequencyMillis = resources.getInteger(R.integer.config_ambientDarkModeSamplingFrequencyMillis) // Registered callback, which gets triggered when the ambient light mode changes. private var callback: AmbientLightModeMonitor.Callback? = null // Queue of bundles used for calculating [isLightMode], ordered from oldest to latest. val bundlesQueueLightMode = ArrayList<ArrayList<Float>>() // Queue of bundles used for calculating [isDarkMode], ordered from oldest to latest val bundlesQueueDarkMode = ArrayList<ArrayList<Float>>() // The current ambient light mode. @AmbientLightModeMonitor.AmbientLightMode var mode = AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED set(value) { if (field == value) return field = value if (DEBUG) Log.d(TAG, "ambient light mode changed to $value") callback?.onChange(value) } // The latest claim of whether it should be light mode. var isLightMode = false set(value) { if (field == value) return field = value if (DEBUG) Log.d(TAG, "isLightMode: $value") mode = when { isDarkMode -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK value -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT else -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } } // The latest claim of whether it should be dark mode. var isDarkMode = false set(value) { if (field == value) return field = value if (DEBUG) Log.d(TAG, "isDarkMode: $value") mode = when { value -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK isLightMode -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT else -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } } // The latest average of the light mode bundle. var bundleAverageLightMode = 0.0 set(value) { field = value if (DEBUG) Log.d(TAG, "light mode average: $value") isLightMode = value > lightModeThreshold } // The latest average of the dark mode bundle. var bundleAverageDarkMode = 0.0 set(value) { field = value if (DEBUG) Log.d(TAG, "dark mode average: $value") isDarkMode = value < darkModeThreshold } // The latest bundle for calculating light mode claim. var bundleLightMode = ArrayList<Float>() set(value) { field = value val average = value.average() if (!average.isNaN()) { bundleAverageLightMode = average } } // The latest bundle for calculating dark mode claim. var bundleDarkMode = ArrayList<Float>() set(value) { field = value val average = value.average() if (!average.isNaN()) { bundleAverageDarkMode = average } } // The latest light level from light sensor event updates. var lightSensorLevel = 0.0f set(value) { field = value bundlesQueueLightMode.forEach { bundle -> bundle.add(value) } bundlesQueueDarkMode.forEach { bundle -> bundle.add(value) } } // Creates a new bundle that collects light sensor events for calculating the light mode claim, // and adds it to the end of the queue. It schedules a call to dequeue this bundle after // [LIGHT_SAMPLING_SPAN_MILLIS]. Once started, it also repeatedly calls itself at // [LIGHT_SAMPLING_FREQUENCY_MILLIS]. val enqueueLightModeBundle: Runnable = object : Runnable { override fun run() { if (DEBUG) Log.d(TAG, "enqueueing a light mode bundle") bundlesQueueLightMode.add(ArrayList()) executor.executeDelayed(dequeueLightModeBundle, lightSamplingSpanMillis.toLong()) executor.executeDelayed(this, lightSamplingFrequencyMillis.toLong()) } } // Creates a new bundle that collects light sensor events for calculating the dark mode claim, // and adds it to the end of the queue. It schedules a call to dequeue this bundle after // [DARK_SAMPLING_SPAN_MILLIS]. Once started, it also repeatedly calls itself at // [DARK_SAMPLING_FREQUENCY_MILLIS]. val enqueueDarkModeBundle: Runnable = object : Runnable { override fun run() { if (DEBUG) Log.d(TAG, "enqueueing a dark mode bundle") bundlesQueueDarkMode.add(ArrayList()) executor.executeDelayed(dequeueDarkModeBundle, darkSamplingSpanMillis.toLong()) executor.executeDelayed(this, darkSamplingFrequencyMillis.toLong()) } } // Collects the oldest bundle from the light mode bundles queue, and as a result triggering a // calculation of the light mode claim. val dequeueLightModeBundle: Runnable = object : Runnable { override fun run() { if (bundlesQueueLightMode.isEmpty()) return bundleLightMode = bundlesQueueLightMode.removeAt(0) if (DEBUG) Log.d(TAG, "dequeued a light mode bundle of size ${bundleLightMode.size}") } } // Collects the oldest bundle from the dark mode bundles queue, and as a result triggering a // calculation of the dark mode claim. val dequeueDarkModeBundle: Runnable = object : Runnable { override fun run() { if (bundlesQueueDarkMode.isEmpty()) return bundleDarkMode = bundlesQueueDarkMode.removeAt(0) if (DEBUG) Log.d(TAG, "dequeued a dark mode bundle of size ${bundleDarkMode.size}") } } /** * Start the algorithm. * * @param callback callback that gets triggered when the ambient light mode changes. */ override fun start(callback: AmbientLightModeMonitor.Callback?) { if (DEBUG) Log.d(TAG, "start algorithm") if (callback == null) { if (DEBUG) Log.w(TAG, "callback is null") return } if (this.callback != null) { if (DEBUG) Log.w(TAG, "already started") return } this.callback = callback executor.execute(enqueueLightModeBundle) executor.execute(enqueueDarkModeBundle) } /** * Stop the algorithm. */ override fun stop() { if (DEBUG) Log.d(TAG, "stop algorithm") if (callback == null) { if (DEBUG) Log.w(TAG, "haven't started") return } callback = null // Resets bundle queues. bundlesQueueLightMode.clear() bundlesQueueDarkMode.clear() // Resets ambient light mode. mode = AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } /** * Update the light sensor event value. * * @param value light sensor update value. */ override fun onUpdateLightSensorEvent(value: Float) { if (callback == null) { if (DEBUG) Log.w(TAG, "ignore light sensor event because algorithm not started") return } lightSensorLevel = value } } No newline at end of file packages/SystemUI/tests/src/com/android/systemui/idle/AmbientLightModeMonitorTest.kt +14 −75 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.systemui.idle import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest Loading @@ -29,11 +28,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.reset import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.anyInt import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations Loading @@ -42,6 +40,7 @@ import org.mockito.MockitoAnnotations class AmbientLightModeMonitorTest : SysuiTestCase() { @Mock private lateinit var sensorManager: AsyncSensorManager @Mock private lateinit var sensor: Sensor @Mock private lateinit var algorithm: AmbientLightModeMonitor.DebounceAlgorithm private lateinit var ambientLightModeMonitor: AmbientLightModeMonitor Loading @@ -50,100 +49,45 @@ class AmbientLightModeMonitorTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) `when`(sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(sensor) ambientLightModeMonitor = AmbientLightModeMonitor(sensorManager) ambientLightModeMonitor = AmbientLightModeMonitor(algorithm, sensorManager) } @Test fun shouldTriggerCallbackImmediatelyOnStart() { fun shouldRegisterSensorEventListenerOnStart() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) // Monitor just started, should receive UNDECIDED. verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) // Receives SensorEvent, and now mode is LIGHT. val sensorEventListener = captureSensorEventListener() sensorEventListener.onSensorChanged(sensorEventWithSingleValue(15f)) // Stop monitor. ambientLightModeMonitor.stop() // Restart monitor. reset(callback) ambientLightModeMonitor.start(callback) // Verify receiving current mode (LIGHT) immediately. verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) verify(sensorManager).registerListener(any(), eq(sensor), anyInt()) } @Test fun shouldReportDarkModeWhenSensorValueIsLessThanTen() { fun shouldUnregisterSensorEventListenerOnStop() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) val sensorEventListener = captureSensorEventListener() reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(0f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(1f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(5f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) ambientLightModeMonitor.stop() sensorEventListener.onSensorChanged(sensorEventWithSingleValue(9.9f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) verify(sensorManager).unregisterListener(eq(sensorEventListener)) } @Test fun shouldReportLightModeWhenSensorValueIsGreaterThanOrEqualToTen() { fun shouldStartDebounceAlgorithmOnStart() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) val sensorEventListener = captureSensorEventListener() reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(10f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(10.1f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(15f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(100f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) verify(algorithm).start(eq(callback)) } @Test fun shouldOnlyTriggerCallbackWhenValueChanges() { fun shouldStopDebounceAlgorithmOnStop() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) ambientLightModeMonitor.stop() val sensorEventListener = captureSensorEventListener() // Light mode, should trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(20f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) // Light mode again, should NOT trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(25f)) verify(callback, never()).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) // Dark mode, should trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(2f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) // Dark mode again, should not trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(3f)) verify(callback, never()).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) verify(algorithm).stop() } // Captures [SensorEventListener], assuming it has been registered with [sensorManager]. Loading @@ -152,9 +96,4 @@ class AmbientLightModeMonitorTest : SysuiTestCase() { verify(sensorManager).registerListener(captor.capture(), any(), anyInt()) return captor.value } // Returns a [SensorEvent] with a single [value]. private fun sensorEventWithSingleValue(value: Float): SensorEvent { return SensorEvent(sensor, 1, 1, FloatArray(1) { value }) } } Loading
packages/SystemUI/res/values/config.xml +24 −0 Original line number Diff line number Diff line Loading @@ -723,6 +723,30 @@ <!-- Timeout to idle mode duration in milliseconds. --> <integer name="config_idleModeTimeout">10000</integer> <!-- This value is used when calculating whether the device is in ambient light mode. It is light mode when the light sensor sample value exceeds above this value. --> <integer name="config_ambientLightModeThreshold">5</integer> <!-- This value is used when calculating whether the device is in ambient dark mode. It is dark mode when the light sensor sample value drops below this value. --> <integer name="config_ambientDarkModeThreshold">2</integer> <!-- This value is used when calculating whether the device is in ambient light mode. Each sample contains light sensor events from this span of time duration. --> <integer name="config_ambientLightModeSamplingSpanMillis">10000</integer> <!-- This value is used when calculating whether the device is in ambient dark mode. Each sample contains light sensor events from this span of time duration. --> <integer name="config_ambientDarkModeSamplingSpanMillis">2000</integer> <!-- This value is used when calculating whether the device is in ambient light mode. The samples are collected at this frequency. --> <integer name="config_ambientLightModeSamplingFrequencyMillis">1000</integer> <!-- This value is used when calculating whether the device is in ambient dark mode. The samples are collected at this frequency. --> <integer name="config_ambientDarkModeSamplingFrequencyMillis">500</integer> <!-- The maximum number of attempts to reconnect to the communal source target after failing to connect --> <integer name="config_communalSourceMaxReconnectAttempts">10</integer> Loading
packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java +12 −0 Original line number Diff line number Diff line Loading @@ -20,10 +20,13 @@ import android.content.Context; import android.view.View; import android.widget.FrameLayout; import com.android.systemui.idle.AmbientLightModeMonitor; import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm; import com.android.systemui.idle.dagger.IdleViewComponent; import javax.inject.Named; import dagger.Binds; import dagger.Module; import dagger.Provides; Loading @@ -44,4 +47,13 @@ public interface CommunalModule { FrameLayout view = new FrameLayout(context); return view; } /** * Provides LightSensorEventsDebounceAlgorithm as an instance to DebounceAlgorithm interface. * @param algorithm the instance of algorithm that is bound to the interface. * @return the interface that is bound to. */ @Binds AmbientLightModeMonitor.DebounceAlgorithm ambientLightDebounceAlgorithm( LightSensorEventsDebounceAlgorithm algorithm); }
packages/SystemUI/src/com/android/systemui/idle/AmbientLightModeMonitor.kt +18 −34 Original line number Diff line number Diff line Loading @@ -24,17 +24,17 @@ import android.hardware.SensorManager import android.util.Log import com.android.systemui.util.sensors.AsyncSensorManager import javax.inject.Inject import kotlin.properties.Delegates /** * Monitors ambient light signals, applies a debouncing algorithm, and produces the current * [AmbientLightMode]. * * For debouncer behavior, refer to go/titan-light-sensor-debouncer. * ambient light mode. * * @property algorithm the debounce algorithm which transforms light sensor events into an * ambient light mode. * @property sensorManager the sensor manager used to register sensor event updates. */ class AmbientLightModeMonitor @Inject constructor( private val algorithm: DebounceAlgorithm, private val sensorManager: AsyncSensorManager ) { companion object { Loading @@ -49,39 +49,20 @@ class AmbientLightModeMonitor @Inject constructor( // Light sensor used to detect ambient lighting conditions. private val lightSensor: Sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) // Registered callback, which gets triggered when the ambient light mode changes. private var callback: Callback? = null // Represents all ambient light modes. @Retention(AnnotationRetention.SOURCE) @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) annotation class AmbientLightMode // The current ambient light mode. @AmbientLightMode private var mode: Int by Delegates.observable(AMBIENT_LIGHT_MODE_UNDECIDED ) { _, old, new -> if (old != new) { callback?.onChange(new) } } /** * Start monitoring the current ambient light mode. * * @param callback callback that gets triggered when the ambient light mode changes. It also * gets triggered immediately to update the current value when this function is called. * @param callback callback that gets triggered when the ambient light mode changes. */ fun start(callback: Callback) { if (DEBUG) Log.d(TAG, "start monitoring ambient light mode") if (this.callback != null) { if (DEBUG) Log.w(TAG, "already started") return } this.callback = callback callback.onChange(mode) algorithm.start(callback) sensorManager.registerListener(mSensorEventListener, lightSensor, SensorManager.SENSOR_DELAY_NORMAL) } Loading @@ -92,12 +73,7 @@ class AmbientLightModeMonitor @Inject constructor( fun stop() { if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode") if (callback == null) { if (DEBUG) Log.w(TAG, "haven't started") return } callback = null algorithm.stop() sensorManager.unregisterListener(mSensorEventListener) } Loading @@ -108,9 +84,7 @@ class AmbientLightModeMonitor @Inject constructor( return } // TODO(b/201657509): add debouncing logic. val shouldBeLowLight = event.values[0] < 10 mode = if (shouldBeLowLight) AMBIENT_LIGHT_MODE_DARK else AMBIENT_LIGHT_MODE_LIGHT algorithm.onUpdateLightSensorEvent(event.values[0]) } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { Loading @@ -124,4 +98,14 @@ class AmbientLightModeMonitor @Inject constructor( 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) } } No newline at end of file
packages/SystemUI/src/com/android/systemui/idle/LightSensorEventsDebounceAlgorithm.kt 0 → 100644 +284 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.idle import android.content.res.Resources import android.util.Log import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject /** * An algorithm that receives light sensor events, debounces the signals, and transforms into an * ambient light mode: light, dark, or undecided. * * More about the algorithm at go/titan-light-sensor-debouncer. */ class LightSensorEventsDebounceAlgorithm @Inject constructor( @Main private val executor: DelayableExecutor, @Main resources: Resources ) : AmbientLightModeMonitor.DebounceAlgorithm { companion object { private const val TAG = "LightDebounceAlgorithm" private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) } // The ambient mode is considered light mode when the light sensor value increases exceeding // this value. private val lightModeThreshold = resources.getInteger(R.integer.config_ambientLightModeThreshold) // The ambient mode is considered dark mode when the light sensor value drops below this // value. private val darkModeThreshold = resources.getInteger(R.integer.config_ambientDarkModeThreshold) // Each sample for calculating light mode contains light sensor events collected for this // duration of time in milliseconds. private val lightSamplingSpanMillis = resources.getInteger(R.integer.config_ambientLightModeSamplingSpanMillis) // Each sample for calculating dark mode contains light sensor events collected for this // duration of time in milliseconds. private val darkSamplingSpanMillis = resources.getInteger(R.integer.config_ambientDarkModeSamplingSpanMillis) // The calculations for light mode is performed at this frequency in milliseconds. private val lightSamplingFrequencyMillis = resources.getInteger(R.integer.config_ambientLightModeSamplingFrequencyMillis) // The calculations for dark mode is performed at this frequency in milliseconds. private val darkSamplingFrequencyMillis = resources.getInteger(R.integer.config_ambientDarkModeSamplingFrequencyMillis) // Registered callback, which gets triggered when the ambient light mode changes. private var callback: AmbientLightModeMonitor.Callback? = null // Queue of bundles used for calculating [isLightMode], ordered from oldest to latest. val bundlesQueueLightMode = ArrayList<ArrayList<Float>>() // Queue of bundles used for calculating [isDarkMode], ordered from oldest to latest val bundlesQueueDarkMode = ArrayList<ArrayList<Float>>() // The current ambient light mode. @AmbientLightModeMonitor.AmbientLightMode var mode = AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED set(value) { if (field == value) return field = value if (DEBUG) Log.d(TAG, "ambient light mode changed to $value") callback?.onChange(value) } // The latest claim of whether it should be light mode. var isLightMode = false set(value) { if (field == value) return field = value if (DEBUG) Log.d(TAG, "isLightMode: $value") mode = when { isDarkMode -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK value -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT else -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } } // The latest claim of whether it should be dark mode. var isDarkMode = false set(value) { if (field == value) return field = value if (DEBUG) Log.d(TAG, "isDarkMode: $value") mode = when { value -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK isLightMode -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT else -> AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } } // The latest average of the light mode bundle. var bundleAverageLightMode = 0.0 set(value) { field = value if (DEBUG) Log.d(TAG, "light mode average: $value") isLightMode = value > lightModeThreshold } // The latest average of the dark mode bundle. var bundleAverageDarkMode = 0.0 set(value) { field = value if (DEBUG) Log.d(TAG, "dark mode average: $value") isDarkMode = value < darkModeThreshold } // The latest bundle for calculating light mode claim. var bundleLightMode = ArrayList<Float>() set(value) { field = value val average = value.average() if (!average.isNaN()) { bundleAverageLightMode = average } } // The latest bundle for calculating dark mode claim. var bundleDarkMode = ArrayList<Float>() set(value) { field = value val average = value.average() if (!average.isNaN()) { bundleAverageDarkMode = average } } // The latest light level from light sensor event updates. var lightSensorLevel = 0.0f set(value) { field = value bundlesQueueLightMode.forEach { bundle -> bundle.add(value) } bundlesQueueDarkMode.forEach { bundle -> bundle.add(value) } } // Creates a new bundle that collects light sensor events for calculating the light mode claim, // and adds it to the end of the queue. It schedules a call to dequeue this bundle after // [LIGHT_SAMPLING_SPAN_MILLIS]. Once started, it also repeatedly calls itself at // [LIGHT_SAMPLING_FREQUENCY_MILLIS]. val enqueueLightModeBundle: Runnable = object : Runnable { override fun run() { if (DEBUG) Log.d(TAG, "enqueueing a light mode bundle") bundlesQueueLightMode.add(ArrayList()) executor.executeDelayed(dequeueLightModeBundle, lightSamplingSpanMillis.toLong()) executor.executeDelayed(this, lightSamplingFrequencyMillis.toLong()) } } // Creates a new bundle that collects light sensor events for calculating the dark mode claim, // and adds it to the end of the queue. It schedules a call to dequeue this bundle after // [DARK_SAMPLING_SPAN_MILLIS]. Once started, it also repeatedly calls itself at // [DARK_SAMPLING_FREQUENCY_MILLIS]. val enqueueDarkModeBundle: Runnable = object : Runnable { override fun run() { if (DEBUG) Log.d(TAG, "enqueueing a dark mode bundle") bundlesQueueDarkMode.add(ArrayList()) executor.executeDelayed(dequeueDarkModeBundle, darkSamplingSpanMillis.toLong()) executor.executeDelayed(this, darkSamplingFrequencyMillis.toLong()) } } // Collects the oldest bundle from the light mode bundles queue, and as a result triggering a // calculation of the light mode claim. val dequeueLightModeBundle: Runnable = object : Runnable { override fun run() { if (bundlesQueueLightMode.isEmpty()) return bundleLightMode = bundlesQueueLightMode.removeAt(0) if (DEBUG) Log.d(TAG, "dequeued a light mode bundle of size ${bundleLightMode.size}") } } // Collects the oldest bundle from the dark mode bundles queue, and as a result triggering a // calculation of the dark mode claim. val dequeueDarkModeBundle: Runnable = object : Runnable { override fun run() { if (bundlesQueueDarkMode.isEmpty()) return bundleDarkMode = bundlesQueueDarkMode.removeAt(0) if (DEBUG) Log.d(TAG, "dequeued a dark mode bundle of size ${bundleDarkMode.size}") } } /** * Start the algorithm. * * @param callback callback that gets triggered when the ambient light mode changes. */ override fun start(callback: AmbientLightModeMonitor.Callback?) { if (DEBUG) Log.d(TAG, "start algorithm") if (callback == null) { if (DEBUG) Log.w(TAG, "callback is null") return } if (this.callback != null) { if (DEBUG) Log.w(TAG, "already started") return } this.callback = callback executor.execute(enqueueLightModeBundle) executor.execute(enqueueDarkModeBundle) } /** * Stop the algorithm. */ override fun stop() { if (DEBUG) Log.d(TAG, "stop algorithm") if (callback == null) { if (DEBUG) Log.w(TAG, "haven't started") return } callback = null // Resets bundle queues. bundlesQueueLightMode.clear() bundlesQueueDarkMode.clear() // Resets ambient light mode. mode = AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED } /** * Update the light sensor event value. * * @param value light sensor update value. */ override fun onUpdateLightSensorEvent(value: Float) { if (callback == null) { if (DEBUG) Log.w(TAG, "ignore light sensor event because algorithm not started") return } lightSensorLevel = value } } No newline at end of file
packages/SystemUI/tests/src/com/android/systemui/idle/AmbientLightModeMonitorTest.kt +14 −75 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.systemui.idle import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest Loading @@ -29,11 +28,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.reset import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.anyInt import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations Loading @@ -42,6 +40,7 @@ import org.mockito.MockitoAnnotations class AmbientLightModeMonitorTest : SysuiTestCase() { @Mock private lateinit var sensorManager: AsyncSensorManager @Mock private lateinit var sensor: Sensor @Mock private lateinit var algorithm: AmbientLightModeMonitor.DebounceAlgorithm private lateinit var ambientLightModeMonitor: AmbientLightModeMonitor Loading @@ -50,100 +49,45 @@ class AmbientLightModeMonitorTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) `when`(sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(sensor) ambientLightModeMonitor = AmbientLightModeMonitor(sensorManager) ambientLightModeMonitor = AmbientLightModeMonitor(algorithm, sensorManager) } @Test fun shouldTriggerCallbackImmediatelyOnStart() { fun shouldRegisterSensorEventListenerOnStart() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) // Monitor just started, should receive UNDECIDED. verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) // Receives SensorEvent, and now mode is LIGHT. val sensorEventListener = captureSensorEventListener() sensorEventListener.onSensorChanged(sensorEventWithSingleValue(15f)) // Stop monitor. ambientLightModeMonitor.stop() // Restart monitor. reset(callback) ambientLightModeMonitor.start(callback) // Verify receiving current mode (LIGHT) immediately. verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) verify(sensorManager).registerListener(any(), eq(sensor), anyInt()) } @Test fun shouldReportDarkModeWhenSensorValueIsLessThanTen() { fun shouldUnregisterSensorEventListenerOnStop() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) val sensorEventListener = captureSensorEventListener() reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(0f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(1f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(5f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) ambientLightModeMonitor.stop() sensorEventListener.onSensorChanged(sensorEventWithSingleValue(9.9f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) verify(sensorManager).unregisterListener(eq(sensorEventListener)) } @Test fun shouldReportLightModeWhenSensorValueIsGreaterThanOrEqualToTen() { fun shouldStartDebounceAlgorithmOnStart() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) val sensorEventListener = captureSensorEventListener() reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(10f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(10.1f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(15f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(100f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) verify(algorithm).start(eq(callback)) } @Test fun shouldOnlyTriggerCallbackWhenValueChanges() { fun shouldStopDebounceAlgorithmOnStop() { val callback = mock(AmbientLightModeMonitor.Callback::class.java) ambientLightModeMonitor.start(callback) ambientLightModeMonitor.stop() val sensorEventListener = captureSensorEventListener() // Light mode, should trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(20f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) // Light mode again, should NOT trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(25f)) verify(callback, never()).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) // Dark mode, should trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(2f)) verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) // Dark mode again, should not trigger callback. reset(callback) sensorEventListener.onSensorChanged(sensorEventWithSingleValue(3f)) verify(callback, never()).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) verify(algorithm).stop() } // Captures [SensorEventListener], assuming it has been registered with [sensorManager]. Loading @@ -152,9 +96,4 @@ class AmbientLightModeMonitorTest : SysuiTestCase() { verify(sensorManager).registerListener(captor.capture(), any(), anyInt()) return captor.value } // Returns a [SensorEvent] with a single [value]. private fun sensorEventWithSingleValue(value: Float): SensorEvent { return SensorEvent(sensor, 1, 1, FloatArray(1) { value }) } }