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

Commit c44f5d1c authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add debouncing logic to AmbientLightModeMonitor."

parents 78d787ec eaf6c02b
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -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>
+12 −0
Original line number Diff line number Diff line
@@ -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;

@@ -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);
}
+18 −34
Original line number Diff line number Diff line
@@ -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 {
@@ -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)
    }
@@ -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)
    }

@@ -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) {
@@ -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
+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
+14 −75
Original line number Diff line number Diff line
@@ -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
@@ -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

@@ -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

@@ -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].
@@ -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