Extracting LightSensorController from ABC

No functional changes

Bug: b/322445088
Test: atest DisplayServiceTests

Change-Id: I6720977d81a816dd3ce982791e9d8fba08dd2685
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 4aab9d2..1546010 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -29,9 +29,6 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
@@ -40,20 +37,19 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.Trace;
 import android.util.EventLog;
 import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TimeUtils;
 import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.os.BackgroundThread;
+import com.android.internal.os.Clock;
 import com.android.server.EventLogTags;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 
 import java.io.PrintWriter;
@@ -68,8 +64,6 @@
 public class AutomaticBrightnessController {
     private static final String TAG = "AutomaticBrightnessController";
 
-    private static final boolean DEBUG_PRETEND_LIGHT_SENSOR_ABSENT = false;
-
     public static final int AUTO_BRIGHTNESS_ENABLED = 1;
     public static final int AUTO_BRIGHTNESS_DISABLED = 2;
     public static final int AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE = 3;
@@ -87,32 +81,20 @@
     public static final int AUTO_BRIGHTNESS_MODE_DOZE = 2;
     public static final int AUTO_BRIGHTNESS_MODE_MAX = AUTO_BRIGHTNESS_MODE_DOZE;
 
-    // How long the current sensor reading is assumed to be valid beyond the current time.
-    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
-    // non-zero, which in turn ensures that the total weight is non-zero.
-    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
-
     // Debounce for sampling user-initiated changes in display brightness to ensure
     // the user is satisfied with the result before storing the sample.
     private static final int BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS = 10000;
 
-    private static final int MSG_UPDATE_AMBIENT_LUX = 1;
-    private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 2;
-    private static final int MSG_INVALIDATE_CURRENT_SHORT_TERM_MODEL = 3;
-    private static final int MSG_UPDATE_FOREGROUND_APP = 4;
-    private static final int MSG_UPDATE_FOREGROUND_APP_SYNC = 5;
-    private static final int MSG_RUN_UPDATE = 6;
-    private static final int MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL = 7;
+    private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 1;
+    private static final int MSG_INVALIDATE_CURRENT_SHORT_TERM_MODEL = 2;
+    private static final int MSG_UPDATE_FOREGROUND_APP = 3;
+    private static final int MSG_UPDATE_FOREGROUND_APP_SYNC = 4;
+    private static final int MSG_RUN_UPDATE = 5;
+    private static final int MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL = 6;
 
     // Callbacks for requesting updates to the display's power state
     private final Callbacks mCallbacks;
 
-    // The sensor manager.
-    private final SensorManager mSensorManager;
-
-    // The light sensor, or null if not available or needed.
-    private final Sensor mLightSensor;
-
     // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
     @NonNull
     private BrightnessMappingStrategy mCurrentBrightnessMapper;
@@ -127,94 +109,21 @@
     // How much to scale doze brightness by (should be (0, 1.0]).
     private final float mDozeScaleFactor;
 
-    // Initial light sensor event rate in milliseconds.
-    private final int mInitialLightSensorRate;
-
-    // Steady-state light sensor event rate in milliseconds.
-    private final int mNormalLightSensorRate;
-
-    // The current light sensor event rate in milliseconds.
-    private int mCurrentLightSensorRate;
-
-    // Stability requirements in milliseconds for accepting a new brightness level.  This is used
-    // for debouncing the light sensor.  Different constants are used to debounce the light sensor
-    // when adapting to brighter or darker environments.  This parameter controls how quickly
-    // brightness changes occur in response to an observed change in light level that exceeds the
-    // hysteresis threshold.
-    private final long mBrighteningLightDebounceConfig;
-    private final long mDarkeningLightDebounceConfig;
-    private final long mBrighteningLightDebounceConfigIdle;
-    private final long mDarkeningLightDebounceConfigIdle;
-
-    // If true immediately after the screen is turned on the controller will try to adjust the
-    // brightness based on the current sensor reads. If false, the controller will collect more data
-    // and only then decide whether to change brightness.
-    private final boolean mResetAmbientLuxAfterWarmUpConfig;
-
-    // Period of time in which to consider light samples for a short/long-term estimate of ambient
-    // light in milliseconds.
-    private final int mAmbientLightHorizonLong;
-    private final int mAmbientLightHorizonShort;
-
-    // The intercept used for the weighting calculation. This is used in order to keep all possible
-    // weighting values positive.
-    private final int mWeightingIntercept;
-
     // Configuration object for determining thresholds to change brightness dynamically
-    private final HysteresisLevels mAmbientBrightnessThresholds;
     private final HysteresisLevels mScreenBrightnessThresholds;
-    private final HysteresisLevels mAmbientBrightnessThresholdsIdle;
     private final HysteresisLevels mScreenBrightnessThresholdsIdle;
 
     private boolean mLoggingEnabled;
-
-    // Amount of time to delay auto-brightness after screen on while waiting for
-    // the light sensor to warm-up in milliseconds.
-    // May be 0 if no warm-up is required.
-    private int mLightSensorWarmUpTimeConfig;
-
-    // Set to true if the light sensor is enabled.
-    private boolean mLightSensorEnabled;
-
-    // The time when the light sensor was enabled.
-    private long mLightSensorEnableTime;
-
     // The currently accepted nominal ambient light level.
     private float mAmbientLux;
-
-    // The last calculated ambient light level (long time window).
-    private float mSlowAmbientLux;
-
-    // The last calculated ambient light level (short time window).
-    private float mFastAmbientLux;
-
-    // The last ambient lux value prior to passing the darkening or brightening threshold.
-    private float mPreThresholdLux;
-
     // True if mAmbientLux holds a valid value.
     private boolean mAmbientLuxValid;
-
-    // The ambient light level threshold at which to brighten or darken the screen.
-    private float mAmbientBrighteningThreshold;
-    private float mAmbientDarkeningThreshold;
-
     // The last brightness value prior to passing the darkening or brightening threshold.
     private float mPreThresholdBrightness;
 
     // The screen brightness threshold at which to brighten or darken the screen.
     private float mScreenBrighteningThreshold;
     private float mScreenDarkeningThreshold;
-    // The most recent light sample.
-    private float mLastObservedLux = INVALID_LUX;
-
-    // The time of the most light recent sample.
-    private long mLastObservedLuxTime;
-
-    // The number of light samples collected since the light sensor was enabled.
-    private int mRecentLightSamples;
-
-    // A ring buffer containing all of the recent ambient light sensor readings.
-    private AmbientLightRingBuffer mAmbientLightRingBuffer;
 
     // The handler
     private AutomaticBrightnessHandler mHandler;
@@ -273,88 +182,55 @@
     private Context mContext;
     private int mState = AUTO_BRIGHTNESS_DISABLED;
 
-    private Clock mClock;
+    private final Clock mClock;
     private final Injector mInjector;
 
-    AutomaticBrightnessController(Callbacks callbacks, Looper looper,
-            SensorManager sensorManager, Sensor lightSensor,
+    private final LightSensorController mLightSensorController;
+
+    AutomaticBrightnessController(Callbacks callbacks, Looper looper, SensorManager sensorManager,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-            int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-            float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-            long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-            long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-            boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels ambientBrightnessThresholds,
+            float brightnessMin, float brightnessMax, float dozeScaleFactor,
             HysteresisLevels screenBrightnessThresholds,
-            HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
-            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userNits,
+            BrightnessThrottler brightnessThrottler, float userLux, float userNits,
+            int displayId, LightSensorController.LightSensorControllerConfig config,
             BrightnessClamperController brightnessClamperController) {
-        this(new Injector(), callbacks, looper, sensorManager, lightSensor,
-                brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
-                dozeScaleFactor, lightSensorRate, initialLightSensorRate,
-                brighteningLightDebounceConfig, darkeningLightDebounceConfig,
-                brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
-                resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
-                screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+        this(new Injector(), callbacks, looper,
+                brightnessMappingStrategyMap, brightnessMin, brightnessMax, dozeScaleFactor,
+                screenBrightnessThresholds,
                 screenBrightnessThresholdsIdle, context, brightnessModeController,
-                brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                userNits, brightnessClamperController
+                brightnessThrottler, userLux, userNits,
+                new LightSensorController(sensorManager, looper, displayId, config),
+                brightnessClamperController
         );
     }
 
     @VisibleForTesting
     AutomaticBrightnessController(Injector injector, Callbacks callbacks, Looper looper,
-            SensorManager sensorManager, Sensor lightSensor,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-            int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-            float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-            long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-            long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-            boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels ambientBrightnessThresholds,
+            float brightnessMin, float brightnessMax, float dozeScaleFactor,
             HysteresisLevels screenBrightnessThresholds,
-            HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessRangeController,
-            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userNits,
+            BrightnessThrottler brightnessThrottler, float userLux, float userNits,
+            LightSensorController lightSensorController,
             BrightnessClamperController brightnessClamperController) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
         mCallbacks = callbacks;
-        mSensorManager = sensorManager;
         mCurrentBrightnessMapper = brightnessMappingStrategyMap.get(AUTO_BRIGHTNESS_MODE_DEFAULT);
         mScreenBrightnessRangeMinimum = brightnessMin;
         mScreenBrightnessRangeMaximum = brightnessMax;
-        mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
         mDozeScaleFactor = dozeScaleFactor;
-        mNormalLightSensorRate = lightSensorRate;
-        mInitialLightSensorRate = initialLightSensorRate;
-        mCurrentLightSensorRate = -1;
-        mBrighteningLightDebounceConfig = brighteningLightDebounceConfig;
-        mDarkeningLightDebounceConfig = darkeningLightDebounceConfig;
-        mBrighteningLightDebounceConfigIdle = brighteningLightDebounceConfigIdle;
-        mDarkeningLightDebounceConfigIdle = darkeningLightDebounceConfigIdle;
-        mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
-        mAmbientLightHorizonLong = ambientLightHorizonLong;
-        mAmbientLightHorizonShort = ambientLightHorizonShort;
-        mWeightingIntercept = ambientLightHorizonLong;
-        mAmbientBrightnessThresholds = ambientBrightnessThresholds;
-        mAmbientBrightnessThresholdsIdle = ambientBrightnessThresholdsIdle;
         mScreenBrightnessThresholds = screenBrightnessThresholds;
         mScreenBrightnessThresholdsIdle = screenBrightnessThresholdsIdle;
         mShortTermModel = new ShortTermModel();
         mPausedShortTermModel = new ShortTermModel();
         mHandler = new AutomaticBrightnessHandler(looper);
-        mAmbientLightRingBuffer =
-            new AmbientLightRingBuffer(mNormalLightSensorRate, mAmbientLightHorizonLong, mClock);
-
-        if (!DEBUG_PRETEND_LIGHT_SENSOR_ABSENT) {
-            mLightSensor = lightSensor;
-        }
-
+        mLightSensorController = lightSensorController;
+        mLightSensorController.setListener(this::setAmbientLux);
         mActivityTaskManager = ActivityTaskManager.getService();
         mPackageManager = mContext.getPackageManager();
         mTaskStackListener = new TaskStackListenerImpl();
@@ -406,13 +282,13 @@
         if (brightnessEvent != null) {
             brightnessEvent.setLux(
                     mAmbientLuxValid ? mAmbientLux : PowerManager.BRIGHTNESS_INVALID_FLOAT);
-            brightnessEvent.setPreThresholdLux(mPreThresholdLux);
             brightnessEvent.setPreThresholdBrightness(mPreThresholdBrightness);
             brightnessEvent.setRecommendedBrightness(mScreenAutoBrightness);
             brightnessEvent.setFlags(brightnessEvent.getFlags()
                     | (!mAmbientLuxValid ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
             brightnessEvent.setAutoBrightnessMode(getMode());
+            mLightSensorController.updateBrightnessEvent(brightnessEvent);
         }
 
         if (!mAmbientLuxValid) {
@@ -435,21 +311,22 @@
      */
     public float getAutomaticScreenBrightnessBasedOnLastObservedLux(
             BrightnessEvent brightnessEvent) {
-        if (mLastObservedLux == INVALID_LUX) {
+        float lastObservedLux = mLightSensorController.getLastObservedLux();
+        if (lastObservedLux == INVALID_LUX) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
 
-        float brightness = mCurrentBrightnessMapper.getBrightness(mLastObservedLux,
+        float brightness = mCurrentBrightnessMapper.getBrightness(lastObservedLux,
                 mForegroundAppPackageName, mForegroundAppCategory);
         if (shouldApplyDozeScaleFactor()) {
             brightness *= mDozeScaleFactor;
         }
 
         if (brightnessEvent != null) {
-            brightnessEvent.setLux(mLastObservedLux);
+            brightnessEvent.setLux(lastObservedLux);
             brightnessEvent.setRecommendedBrightness(brightness);
             brightnessEvent.setFlags(brightnessEvent.getFlags()
-                    | (mLastObservedLux == INVALID_LUX ? BrightnessEvent.FLAG_INVALID_LUX : 0)
+                    | (lastObservedLux == INVALID_LUX ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
             brightnessEvent.setAutoBrightnessMode(getMode());
         }
@@ -501,6 +378,7 @@
 
     public void stop() {
         setLightSensorEnabled(false);
+        mLightSensorController.stop();
     }
 
     public boolean hasUserDataPoints() {
@@ -529,14 +407,6 @@
         return mAmbientLux;
     }
 
-    float getSlowAmbientLux() {
-        return mSlowAmbientLux;
-    }
-
-    float getFastAmbientLux() {
-        return mFastAmbientLux;
-    }
-
     private boolean setDisplayPolicy(int policy) {
         if (mDisplayPolicy == policy) {
             return false;
@@ -615,36 +485,13 @@
         pw.println("  mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
         pw.println("  mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
         pw.println("  mDozeScaleFactor=" + mDozeScaleFactor);
-        pw.println("  mInitialLightSensorRate=" + mInitialLightSensorRate);
-        pw.println("  mNormalLightSensorRate=" + mNormalLightSensorRate);
-        pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
-        pw.println("  mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig);
-        pw.println("  mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig);
-        pw.println("  mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle);
-        pw.println("  mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle);
-        pw.println("  mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig);
-        pw.println("  mAmbientLightHorizonLong=" + mAmbientLightHorizonLong);
-        pw.println("  mAmbientLightHorizonShort=" + mAmbientLightHorizonShort);
-        pw.println("  mWeightingIntercept=" + mWeightingIntercept);
-
         pw.println();
         pw.println("Automatic Brightness Controller State:");
-        pw.println("  mLightSensor=" + mLightSensor);
-        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
-        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
-        pw.println("  mCurrentLightSensorRate=" + mCurrentLightSensorRate);
         pw.println("  mAmbientLux=" + mAmbientLux);
         pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
-        pw.println("  mPreThresholdLux=" + mPreThresholdLux);
         pw.println("  mPreThresholdBrightness=" + mPreThresholdBrightness);
-        pw.println("  mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold);
-        pw.println("  mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold);
         pw.println("  mScreenBrighteningThreshold=" + mScreenBrighteningThreshold);
         pw.println("  mScreenDarkeningThreshold=" + mScreenDarkeningThreshold);
-        pw.println("  mLastObservedLux=" + mLastObservedLux);
-        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
-        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
-        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
         pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
         pw.println("  mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy));
         pw.println("  mShortTermModel=");
@@ -673,22 +520,21 @@
         }
 
         pw.println();
-        pw.println("  mAmbientBrightnessThresholds=");
-        mAmbientBrightnessThresholds.dump(pw);
         pw.println("  mScreenBrightnessThresholds=");
         mScreenBrightnessThresholds.dump(pw);
         pw.println("  mScreenBrightnessThresholdsIdle=");
         mScreenBrightnessThresholdsIdle.dump(pw);
-        pw.println("  mAmbientBrightnessThresholdsIdle=");
-        mAmbientBrightnessThresholdsIdle.dump(pw);
+
+        pw.println();
+        mLightSensorController.dump(pw);
     }
 
     public float[] getLastSensorValues() {
-        return mAmbientLightRingBuffer.getAllLuxValues();
+        return mLightSensorController.getLastSensorValues();
     }
 
     public long[] getLastSensorTimestamps() {
-        return mAmbientLightRingBuffer.getAllTimestamps();
+        return mLightSensorController.getLastSensorTimestamps();
     }
 
     private String configStateToString(int state) {
@@ -705,273 +551,33 @@
     }
 
     private boolean setLightSensorEnabled(boolean enable) {
-        if (enable) {
-            if (!mLightSensorEnabled) {
-                mLightSensorEnabled = true;
-                mLightSensorEnableTime = mClock.uptimeMillis();
-                mCurrentLightSensorRate = mInitialLightSensorRate;
-                registerForegroundAppUpdater();
-                mSensorManager.registerListener(mLightSensorListener, mLightSensor,
-                        mCurrentLightSensorRate * 1000, mHandler);
-                return true;
-            }
-        } else if (mLightSensorEnabled) {
-            mLightSensorEnabled = false;
-            mAmbientLuxValid = !mResetAmbientLuxAfterWarmUpConfig;
-            if (!mAmbientLuxValid) {
-                mPreThresholdLux = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            }
+        if (enable && mLightSensorController.enableLightSensorIfNeeded()) {
+            registerForegroundAppUpdater();
+            return true;
+        } else if (!enable && mLightSensorController.disableLightSensorIfNeeded()) {
             mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mRawScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            mRecentLightSamples = 0;
-            mAmbientLightRingBuffer.clear();
-            mCurrentLightSensorRate = -1;
-            mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
+            mAmbientLuxValid = mLightSensorController.hasValidAmbientLux();
             unregisterForegroundAppUpdater();
-            mSensorManager.unregisterListener(mLightSensorListener);
         }
         return false;
     }
 
-    private void handleLightSensorEvent(long time, float lux) {
-        Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux);
-        mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
-
-        if (mAmbientLightRingBuffer.size() == 0) {
-            // switch to using the steady-state sample rate after grabbing the initial light sample
-            adjustLightSensorRate(mNormalLightSensorRate);
-        }
-        applyLightSensorMeasurement(time, lux);
-        updateAmbientLux(time);
-    }
-
-    private void applyLightSensorMeasurement(long time, float lux) {
-        mRecentLightSamples++;
-        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
-        mAmbientLightRingBuffer.push(time, lux);
-
-        // Remember this sample value.
-        mLastObservedLux = lux;
-        mLastObservedLuxTime = time;
-    }
-
-    private void adjustLightSensorRate(int lightSensorRate) {
-        // if the light sensor rate changed, update the sensor listener
-        if (lightSensorRate != mCurrentLightSensorRate) {
-            if (mLoggingEnabled) {
-                Slog.d(TAG, "adjustLightSensorRate: " +
-                        "previousRate=" + mCurrentLightSensorRate + ", " +
-                        "currentRate=" + lightSensorRate);
-            }
-            mCurrentLightSensorRate = lightSensorRate;
-            mSensorManager.unregisterListener(mLightSensorListener);
-            mSensorManager.registerListener(mLightSensorListener, mLightSensor,
-                    lightSensorRate * 1000, mHandler);
-        }
-    }
-
     private boolean setAutoBrightnessAdjustment(float adjustment) {
         return mCurrentBrightnessMapper.setAutoBrightnessAdjustment(adjustment);
     }
 
     private void setAmbientLux(float lux) {
-        if (mLoggingEnabled) {
-            Slog.d(TAG, "setAmbientLux(" + lux + ")");
-        }
-        if (lux < 0) {
-            Slog.w(TAG, "Ambient lux was negative, ignoring and setting to 0");
-            lux = 0;
-        }
+        // called by LightSensorController.setAmbientLux
+        mAmbientLuxValid = true;
         mAmbientLux = lux;
-        if (isInIdleMode()) {
-            mAmbientBrighteningThreshold =
-                    mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(lux);
-            mAmbientDarkeningThreshold =
-                    mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(lux);
-        } else {
-            mAmbientBrighteningThreshold =
-                    mAmbientBrightnessThresholds.getBrighteningThreshold(lux);
-            mAmbientDarkeningThreshold =
-                    mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
-        }
         mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
         mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
 
         // If the short term model was invalidated and the change is drastic enough, reset it.
         mShortTermModel.maybeReset(mAmbientLux);
-    }
-
-    private float calculateAmbientLux(long now, long horizon) {
-        if (mLoggingEnabled) {
-            Slog.d(TAG, "calculateAmbientLux(" + now + ", " + horizon + ")");
-        }
-        final int N = mAmbientLightRingBuffer.size();
-        if (N == 0) {
-            Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
-            return -1;
-        }
-
-        // Find the first measurement that is just outside of the horizon.
-        int endIndex = 0;
-        final long horizonStartTime = now - horizon;
-        for (int i = 0; i < N-1; i++) {
-            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
-                endIndex++;
-            } else {
-                break;
-            }
-        }
-        if (mLoggingEnabled) {
-            Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
-                    mAmbientLightRingBuffer.getTime(endIndex) + ", " +
-                    mAmbientLightRingBuffer.getLux(endIndex) + ")");
-        }
-        float sum = 0;
-        float totalWeight = 0;
-        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
-        for (int i = N - 1; i >= endIndex; i--) {
-            long eventTime = mAmbientLightRingBuffer.getTime(i);
-            if (i == endIndex && eventTime < horizonStartTime) {
-                // If we're at the final value, make sure we only consider the part of the sample
-                // within our desired horizon.
-                eventTime = horizonStartTime;
-            }
-            final long startTime = eventTime - now;
-            float weight = calculateWeight(startTime, endTime);
-            float lux = mAmbientLightRingBuffer.getLux(i);
-            if (mLoggingEnabled) {
-                Slog.d(TAG, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
-                        "lux=" + lux + ", " +
-                        "weight=" + weight);
-            }
-            totalWeight += weight;
-            sum += lux * weight;
-            endTime = startTime;
-        }
-        if (mLoggingEnabled) {
-            Slog.d(TAG, "calculateAmbientLux: " +
-                    "totalWeight=" + totalWeight + ", " +
-                    "newAmbientLux=" + (sum / totalWeight));
-        }
-        return sum / totalWeight;
-    }
-
-    private float calculateWeight(long startDelta, long endDelta) {
-        return weightIntegral(endDelta) - weightIntegral(startDelta);
-    }
-
-    // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the
-    // horizon we're looking at and provides a non-linear weighting for light samples.
-    private float weightIntegral(long x) {
-        return x * (x * 0.5f + mWeightingIntercept);
-    }
-
-    private long nextAmbientLightBrighteningTransition(long time) {
-        final int N = mAmbientLightRingBuffer.size();
-        long earliestValidTime = time;
-        for (int i = N - 1; i >= 0; i--) {
-            if (mAmbientLightRingBuffer.getLux(i) <= mAmbientBrighteningThreshold) {
-                break;
-            }
-            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
-        }
-        return earliestValidTime + (isInIdleMode()
-                ? mBrighteningLightDebounceConfigIdle : mBrighteningLightDebounceConfig);
-    }
-
-    private long nextAmbientLightDarkeningTransition(long time) {
-        final int N = mAmbientLightRingBuffer.size();
-        long earliestValidTime = time;
-        for (int i = N - 1; i >= 0; i--) {
-            if (mAmbientLightRingBuffer.getLux(i) >= mAmbientDarkeningThreshold) {
-                break;
-            }
-            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
-        }
-        return earliestValidTime + (isInIdleMode()
-                ? mDarkeningLightDebounceConfigIdle : mDarkeningLightDebounceConfig);
-    }
-
-    private void updateAmbientLux() {
-        long time = mClock.uptimeMillis();
-        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
-        updateAmbientLux(time);
-    }
-
-    private void updateAmbientLux(long time) {
-        // If the light sensor was just turned on then immediately update our initial
-        // estimate of the current ambient light level.
-        if (!mAmbientLuxValid) {
-            final long timeWhenSensorWarmedUp =
-                mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
-            if (time < timeWhenSensorWarmedUp) {
-                if (mLoggingEnabled) {
-                    Slog.d(TAG, "updateAmbientLux: Sensor not ready yet: "
-                            + "time=" + time + ", "
-                            + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
-                }
-                mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
-                        timeWhenSensorWarmedUp);
-                return;
-            }
-            setAmbientLux(calculateAmbientLux(time, mAmbientLightHorizonShort));
-            mAmbientLuxValid = true;
-            if (mLoggingEnabled) {
-                Slog.d(TAG, "updateAmbientLux: Initializing: " +
-                        "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
-                        "mAmbientLux=" + mAmbientLux);
-            }
-            updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
-        }
-
-        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
-        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
-        // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term
-        // change in lighting conditions, and a fast ambient lux to determine what the new
-        // brightness situation is since the slow lux can be quite slow to converge.
-        //
-        // Note that both values need to be checked for sufficient change before updating the
-        // proposed ambient light value since the slow value might be sufficiently far enough away
-        // from the fast value to cause a recalculation while its actually just converging on
-        // the fast value still.
-        mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong);
-        mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort);
-
-        if ((mSlowAmbientLux >= mAmbientBrighteningThreshold
-                && mFastAmbientLux >= mAmbientBrighteningThreshold
-                && nextBrightenTransition <= time)
-                || (mSlowAmbientLux <= mAmbientDarkeningThreshold
-                        && mFastAmbientLux <= mAmbientDarkeningThreshold
-                        && nextDarkenTransition <= time)) {
-            mPreThresholdLux = mAmbientLux;
-            setAmbientLux(mFastAmbientLux);
-            if (mLoggingEnabled) {
-                Slog.d(TAG, "updateAmbientLux: "
-                        + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
-                        + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", "
-                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
-                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
-                        + "mAmbientLux=" + mAmbientLux);
-            }
-            updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
-            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
-            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
-        }
-        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
-        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
-        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
-        // now. Rather than continually checking to see whether the weighted lux exceeds the
-        // threshold, schedule an update for when we'd normally expect another light sample, which
-        // should be enough time to decide whether we should actually transition to the new
-        // weighted ambient lux or not.
-        nextTransitionTime =
-                nextTransitionTime > time ? nextTransitionTime : time + mNormalLightSensorRate;
-        if (mLoggingEnabled) {
-            Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " +
-                    nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
-        }
-        mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
+        updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
     }
 
     private void updateAutoBrightness(boolean sendUpdate, boolean isManuallySet) {
@@ -1072,8 +678,7 @@
                 if (mLoggingEnabled) {
                     Slog.d(TAG, "Auto-brightness adjustment changed by user: "
                             + "lux=" + mAmbientLux + ", "
-                            + "brightness=" + mScreenAutoBrightness + ", "
-                            + "ring=" + mAmbientLightRingBuffer);
+                            + "brightness=" + mScreenAutoBrightness);
                 }
 
                 EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ,
@@ -1203,6 +808,7 @@
         if (mode == AUTO_BRIGHTNESS_MODE_IDLE
                 || mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) {
             switchModeAndShortTermModels(mode);
+            mLightSensorController.setIdleMode(isInIdleMode());
         } else {
             resetShortTermModel();
             mCurrentBrightnessMapper = mBrightnessMappingStrategyMap.get(mode);
@@ -1359,10 +965,6 @@
                     updateAutoBrightness(true /*sendUpdate*/, false /*isManuallySet*/);
                     break;
 
-                case MSG_UPDATE_AMBIENT_LUX:
-                    updateAmbientLux();
-                    break;
-
                 case MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE:
                     collectBrightnessAdjustmentSample();
                     break;
@@ -1386,22 +988,6 @@
         }
     }
 
-    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mLightSensorEnabled) {
-                final long time = mClock.uptimeMillis();
-                final float lux = event.values[0];
-                handleLightSensorEvent(time, lux);
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
     // Call back whenever the tasks stack changes, which includes tasks being created, removed, and
     // moving to top.
     class TaskStackListenerImpl extends TaskStackListener {
@@ -1416,192 +1002,13 @@
         void updateBrightness();
     }
 
-    /** Functional interface for providing time. */
-    @VisibleForTesting
-    interface Clock {
-        /**
-         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
-         */
-        long uptimeMillis();
-    }
-
-    /**
-     * A ring buffer of ambient light measurements sorted by time.
-     *
-     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
-     * from oldest to newest.
-     */
-    private static final class AmbientLightRingBuffer {
-        // Proportional extra capacity of the buffer beyond the expected number of light samples
-        // in the horizon
-        private static final float BUFFER_SLACK = 1.5f;
-        private float[] mRingLux;
-        private long[] mRingTime;
-        private int mCapacity;
-
-        // The first valid element and the next open slot.
-        // Note that if mCount is zero then there are no valid elements.
-        private int mStart;
-        private int mEnd;
-        private int mCount;
-        Clock mClock;
-
-        public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon, Clock clock) {
-            if (lightSensorRate <= 0) {
-                throw new IllegalArgumentException("lightSensorRate must be above 0");
-            }
-            mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate);
-            mRingLux = new float[mCapacity];
-            mRingTime = new long[mCapacity];
-            mClock = clock;
-        }
-
-        public float getLux(int index) {
-            return mRingLux[offsetOf(index)];
-        }
-
-        public float[] getAllLuxValues() {
-            float[] values = new float[mCount];
-            if (mCount == 0) {
-                return values;
-            }
-
-            if (mStart < mEnd) {
-                System.arraycopy(mRingLux, mStart, values, 0, mCount);
-            } else {
-                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
-                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
-            }
-
-            return values;
-        }
-
-        public long getTime(int index) {
-            return mRingTime[offsetOf(index)];
-        }
-
-        public long[] getAllTimestamps() {
-            long[] values = new long[mCount];
-            if (mCount == 0) {
-                return values;
-            }
-
-            if (mStart < mEnd) {
-                System.arraycopy(mRingTime, mStart, values, 0, mCount);
-            } else {
-                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
-                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
-            }
-
-            return values;
-        }
-
-        public void push(long time, float lux) {
-            int next = mEnd;
-            if (mCount == mCapacity) {
-                int newSize = mCapacity * 2;
-
-                float[] newRingLux = new float[newSize];
-                long[] newRingTime = new long[newSize];
-                int length = mCapacity - mStart;
-                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
-                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
-                if (mStart != 0) {
-                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
-                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
-                }
-                mRingLux = newRingLux;
-                mRingTime = newRingTime;
-
-                next = mCapacity;
-                mCapacity = newSize;
-                mStart = 0;
-            }
-            mRingTime[next] = time;
-            mRingLux[next] = lux;
-            mEnd = next + 1;
-            if (mEnd == mCapacity) {
-                mEnd = 0;
-            }
-            mCount++;
-        }
-
-        public void prune(long horizon) {
-            if (mCount == 0) {
-                return;
-            }
-
-            while (mCount > 1) {
-                int next = mStart + 1;
-                if (next >= mCapacity) {
-                    next -= mCapacity;
-                }
-                if (mRingTime[next] > horizon) {
-                    // Some light sensors only produce data upon a change in the ambient light
-                    // levels, so we need to consider the previous measurement as the ambient light
-                    // level for all points in time up until we receive a new measurement. Thus, we
-                    // always want to keep the youngest element that would be removed from the
-                    // buffer and just set its measurement time to the horizon time since at that
-                    // point it is the ambient light level, and to remove it would be to drop a
-                    // valid data point within our horizon.
-                    break;
-                }
-                mStart = next;
-                mCount -= 1;
-            }
-
-            if (mRingTime[mStart] < horizon) {
-                mRingTime[mStart] = horizon;
-            }
-        }
-
-        public int size() {
-            return mCount;
-        }
-
-        public void clear() {
-            mStart = 0;
-            mEnd = 0;
-            mCount = 0;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder buf = new StringBuilder();
-            buf.append('[');
-            for (int i = 0; i < mCount; i++) {
-                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
-                if (i != 0) {
-                    buf.append(", ");
-                }
-                buf.append(getLux(i));
-                buf.append(" / ");
-                buf.append(next - getTime(i));
-                buf.append("ms");
-            }
-            buf.append(']');
-            return buf.toString();
-        }
-
-        private int offsetOf(int index) {
-            if (index >= mCount || index < 0) {
-                throw new ArrayIndexOutOfBoundsException(index);
-            }
-            index += mStart;
-            if (index >= mCapacity) {
-                index -= mCapacity;
-            }
-            return index;
-        }
-    }
-
     public static class Injector {
         public Handler getBackgroundThreadHandler() {
             return BackgroundThread.getHandler();
         }
 
         Clock createClock() {
-            return SystemClock::uptimeMillis;
+            return Clock.SYSTEM_CLOCK;
         }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 90ad8c0..0807cc0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -81,6 +81,7 @@
 import com.android.server.display.brightness.BrightnessReason;
 import com.android.server.display.brightness.BrightnessUtils;
 import com.android.server.display.brightness.DisplayBrightnessController;
+import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
@@ -1048,102 +1049,13 @@
         }
 
         if (defaultModeBrightnessMapper != null) {
-            // Ambient Lux - Active Mode Brightness Thresholds
-            float[] ambientBrighteningThresholds =
-                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
-            float[] ambientDarkeningThresholds =
-                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
-            float[] ambientBrighteningLevels =
-                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
-            float[] ambientDarkeningLevels =
-                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
-            float ambientDarkeningMinThreshold =
-                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
-            float ambientBrighteningMinThreshold =
-                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
-            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
-                    ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
-                    ambientBrighteningMinThreshold);
-
             // Display - Active Mode Brightness Thresholds
-            float[] screenBrighteningThresholds =
-                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
-            float[] screenDarkeningThresholds =
-                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
-            float[] screenBrighteningLevels =
-                    mDisplayDeviceConfig.getScreenBrighteningLevels();
-            float[] screenDarkeningLevels =
-                    mDisplayDeviceConfig.getScreenDarkeningLevels();
-            float screenDarkeningMinThreshold =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
-            float screenBrighteningMinThreshold =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
-            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds,
-                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
-                    screenBrighteningMinThreshold, true);
-
-            // Ambient Lux - Idle Screen Brightness Thresholds
-            float ambientDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
-            float ambientBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
-            float[] ambientBrighteningThresholdsIdle =
-                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
-            float[] ambientDarkeningThresholdsIdle =
-                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
-            float[] ambientBrighteningLevelsIdle =
-                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
-            float[] ambientDarkeningLevelsIdle =
-                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
-            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
-                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
-                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
-                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
+            HysteresisLevels screenBrightnessThresholds =
+                    mInjector.getBrightnessThresholdsHysteresisLevels(mDisplayDeviceConfig);
 
             // Display - Idle Screen Brightness Thresholds
-            float screenDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
-            float screenBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
-            float[] screenBrighteningThresholdsIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
-            float[] screenDarkeningThresholdsIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
-            float[] screenBrighteningLevelsIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
-            float[] screenDarkeningLevelsIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
-                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
-                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
-                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
-            long brighteningLightDebounce = mDisplayDeviceConfig
-                    .getAutoBrightnessBrighteningLightDebounce();
-            long darkeningLightDebounce = mDisplayDeviceConfig
-                    .getAutoBrightnessDarkeningLightDebounce();
-            long brighteningLightDebounceIdle = mDisplayDeviceConfig
-                    .getAutoBrightnessBrighteningLightDebounceIdle();
-            long darkeningLightDebounceIdle = mDisplayDeviceConfig
-                    .getAutoBrightnessDarkeningLightDebounceIdle();
-            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
-                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
-
-            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
-                    R.integer.config_lightSensorWarmupTime);
-            int lightSensorRate = context.getResources().getInteger(
-                    R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = context.getResources().getInteger(
-                    R.integer.config_autoBrightnessInitialLightSensorRate);
-            if (initialLightSensorRate == -1) {
-                initialLightSensorRate = lightSensorRate;
-            } else if (initialLightSensorRate > lightSensorRate) {
-                Slog.w(mTag, "Expected config_autoBrightnessInitialLightSensorRate ("
-                        + initialLightSensorRate + ") to be less than or equal to "
-                        + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
-            }
+            HysteresisLevels screenBrightnessThresholdsIdle =
+                    mInjector.getBrightnessThresholdsIdleHysteresisLevels(mDisplayDeviceConfig);
 
             loadAmbientLightSensor();
             // BrightnessTracker should only use one light sensor, we want to use the light sensor
@@ -1155,17 +1067,15 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
+
+            LightSensorController.LightSensorControllerConfig config =
+                    mInjector.getLightSensorControllerConfig(context, mDisplayDeviceConfig);
             mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
-                    this, handler.getLooper(), mSensorManager, mLightSensor,
-                    brightnessMappers, lightSensorWarmUpTimeConfig, PowerManager.BRIGHTNESS_MIN,
-                    PowerManager.BRIGHTNESS_MAX, mDozeScaleFactor, lightSensorRate,
-                    initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
-                    brighteningLightDebounceIdle, darkeningLightDebounceIdle,
-                    autoBrightnessResetAmbientLuxAfterWarmUp, ambientBrightnessThresholds,
-                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
-                    screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
-                    mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
+                    this, handler.getLooper(), mSensorManager, brightnessMappers,
+                    PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, mDozeScaleFactor,
+                    screenBrightnessThresholds, screenBrightnessThresholdsIdle,
+                    mContext, mBrightnessRangeController,
+                    mBrightnessThrottler, userLux, userNits, mDisplayId, config,
                     mBrightnessClamperController);
             mDisplayBrightnessController.setAutomaticBrightnessController(
                     mAutomaticBrightnessController);
@@ -3165,32 +3075,34 @@
 
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager, Sensor lightSensor,
+                SensorManager sensorManager,
                 SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                boolean resetAmbientLuxAfterWarmUpConfig,
-                HysteresisLevels ambientBrightnessThresholds,
+                float brightnessMin, float brightnessMax, float dozeScaleFactor,
                 HysteresisLevels screenBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessModeController,
-                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits,
+                BrightnessThrottler brightnessThrottler, float userLux, float userNits,
+                int displayId, LightSensorController.LightSensorControllerConfig config,
                 BrightnessClamperController brightnessClamperController) {
+            return new AutomaticBrightnessController(callbacks, looper, sensorManager,
+                    brightnessMappingStrategyMap, brightnessMin, brightnessMax, dozeScaleFactor,
+                    screenBrightnessThresholds, screenBrightnessThresholdsIdle, context,
+                    brightnessModeController, brightnessThrottler, userLux, userNits, displayId,
+                    config, brightnessClamperController);
+        }
 
-            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
-                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
-                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
-                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
-                    brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
-                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
-                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
-                    screenBrightnessThresholdsIdle, context, brightnessModeController,
-                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                    userNits, brightnessClamperController);
+        LightSensorController.LightSensorControllerConfig getLightSensorControllerConfig(
+                Context context, DisplayDeviceConfig displayDeviceConfig) {
+            return LightSensorController.LightSensorControllerConfig.create(
+                    context.getResources(), displayDeviceConfig);
+        }
+
+        HysteresisLevels getBrightnessThresholdsIdleHysteresisLevels(DisplayDeviceConfig ddc) {
+            return HysteresisLevels.getBrightnessThresholdsIdle(ddc);
+        }
+
+        HysteresisLevels getBrightnessThresholdsHysteresisLevels(DisplayDeviceConfig ddc) {
+            return HysteresisLevels.getBrightnessThresholds(ddc);
         }
 
         BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
@@ -3200,25 +3112,6 @@
                     AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
         }
 
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold) {
-            return new HysteresisLevels(brighteningThresholdsPercentages,
-                    darkeningThresholdsPercentages, brighteningThresholdLevels,
-                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
-        }
-
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
-            return new HysteresisLevels(brighteningThresholdsPercentages,
-                    darkeningThresholdsPercentages, brighteningThresholdLevels,
-                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
-                    potentialOldBrightnessRange);
-        }
-
         ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
                 SensorManager sensorManager,
                 Sensor lightSensor,
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index 0521b8a..bb349e7 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -18,6 +18,7 @@
 
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
@@ -52,7 +53,8 @@
      * @param potentialOldBrightnessRange whether or not the values used could be from the old
      *                                    screen brightness range ie, between 1-255.
     */
-    HysteresisLevels(float[] brighteningThresholdsPercentages,
+    @VisibleForTesting
+    public HysteresisLevels(float[] brighteningThresholdsPercentages,
             float[] darkeningThresholdsPercentages,
             float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
             float minDarkeningThreshold, float minBrighteningThreshold,
@@ -138,7 +140,10 @@
         return levelArray;
     }
 
-    void dump(PrintWriter pw) {
+    /**
+     * Print the object's debug information into the given stream.
+     */
+    public void dump(PrintWriter pw) {
         pw.println("HysteresisLevels");
         pw.println("  mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels));
         pw.println("  mBrighteningThresholdsPercentages="
@@ -149,4 +154,45 @@
                 + Arrays.toString(mDarkeningThresholdsPercentages));
         pw.println("  mMinDarkening=" + mMinDarkening);
     }
+
+
+    /**
+     * Creates hysteresis levels for Active Ambient Lux
+     */
+    public static HysteresisLevels getAmbientBrightnessThresholds(DisplayDeviceConfig ddc) {
+        return new HysteresisLevels(ddc.getAmbientBrighteningPercentages(),
+                ddc.getAmbientDarkeningPercentages(), ddc.getAmbientBrighteningLevels(),
+                ddc.getAmbientDarkeningLevels(), ddc.getAmbientLuxDarkeningMinThreshold(),
+                ddc.getAmbientLuxBrighteningMinThreshold());
+    }
+
+    /**
+     * Creates hysteresis levels for Active Screen Brightness
+     */
+    public static HysteresisLevels getBrightnessThresholds(DisplayDeviceConfig ddc) {
+        return new HysteresisLevels(ddc.getScreenBrighteningPercentages(),
+                ddc.getScreenDarkeningPercentages(), ddc.getScreenBrighteningLevels(),
+                ddc.getScreenDarkeningLevels(), ddc.getScreenDarkeningMinThreshold(),
+                ddc.getScreenBrighteningMinThreshold(), true);
+    }
+
+    /**
+     * Creates hysteresis levels for Idle Ambient Lux
+     */
+    public static HysteresisLevels getAmbientBrightnessThresholdsIdle(DisplayDeviceConfig ddc) {
+        return new HysteresisLevels(ddc.getAmbientBrighteningPercentagesIdle(),
+                ddc.getAmbientDarkeningPercentagesIdle(), ddc.getAmbientBrighteningLevelsIdle(),
+                ddc.getAmbientDarkeningLevelsIdle(), ddc.getAmbientLuxDarkeningMinThresholdIdle(),
+                ddc.getAmbientLuxBrighteningMinThresholdIdle());
+    }
+
+    /**
+     * Creates hysteresis levels for Idle Screen Brightness
+     */
+    public static HysteresisLevels getBrightnessThresholdsIdle(DisplayDeviceConfig ddc) {
+        return new HysteresisLevels(ddc.getScreenBrighteningPercentagesIdle(),
+                ddc.getScreenDarkeningPercentagesIdle(), ddc.getScreenBrighteningLevelsIdle(),
+                ddc.getScreenDarkeningLevelsIdle(), ddc.getScreenDarkeningMinThresholdIdle(),
+                ddc.getScreenBrighteningMinThresholdIdle());
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/LightSensorController.java b/services/core/java/com/android/server/display/brightness/LightSensorController.java
new file mode 100644
index 0000000..d82d698
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/LightSensorController.java
@@ -0,0 +1,868 @@
+/*
+ * Copyright (C) 2024 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.server.display.brightness;
+
+import static com.android.server.display.BrightnessMappingStrategy.INVALID_LUX;
+
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.Trace;
+import android.util.Slog;
+import android.util.TimeUtils;
+import android.view.Display;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.Clock;
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.HysteresisLevels;
+import com.android.server.display.config.SensorData;
+import com.android.server.display.utils.SensorUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages light sensor subscription and notifies its listeners about ambient lux changes based on
+ * configuration
+ */
+public class LightSensorController {
+    // How long the current sensor reading is assumed to be valid beyond the current time.
+    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
+    // non-zero, which in turn ensures that the total weight is non-zero.
+    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
+
+    // Proportional extra capacity of the buffer beyond the expected number of light samples
+    // in the horizon
+    private static final float BUFFER_SLACK = 1.5f;
+
+    private boolean mLoggingEnabled;
+    private boolean mLightSensorEnabled;
+    private long mLightSensorEnableTime;
+    // The current light sensor event rate in milliseconds.
+    private int mCurrentLightSensorRate = -1;
+    // The number of light samples collected since the light sensor was enabled.
+    private int mRecentLightSamples;
+    private float mAmbientLux;
+    // True if mAmbientLux holds a valid value.
+    private boolean mAmbientLuxValid;
+    // The last ambient lux value prior to passing the darkening or brightening threshold.
+    private float mPreThresholdLux;
+    // The most recent light sample.
+    private float mLastObservedLux = INVALID_LUX;
+    // The time of the most light recent sample.
+    private long mLastObservedLuxTime;
+    // The last calculated ambient light level (long time window).
+    private float mSlowAmbientLux;
+    // The last calculated ambient light level (short time window).
+    private float mFastAmbientLux;
+    private volatile boolean mIsIdleMode;
+    // The ambient light level threshold at which to brighten or darken the screen.
+    private float mAmbientBrighteningThreshold;
+    private float mAmbientDarkeningThreshold;
+
+    private final LightSensorControllerConfig mConfig;
+
+    // The light sensor, or null if not available or needed.
+    @Nullable
+    private final Sensor mLightSensor;
+
+    // A ring buffer containing all of the recent ambient light sensor readings.
+    private final AmbientLightRingBuffer mAmbientLightRingBuffer;
+
+    private final Injector mInjector;
+
+    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (mLightSensorEnabled) {
+                final long time = mClock.uptimeMillis();
+                final float lux = event.values[0];
+                handleLightSensorEvent(time, lux);
+            }
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            // Not used.
+        }
+    };
+
+    // Runnable used to delay ambient lux update when:
+    // 1) update triggered before configured warm up time
+    // 2) next brightening or darkening transition need to happen
+    private final Runnable mAmbientLuxUpdater = this::updateAmbientLux;
+
+    private final Clock mClock;
+
+    private final Handler mHandler;
+
+    private final String mTag;
+
+    private LightSensorListener mListener;
+
+    public LightSensorController(
+            SensorManager sensorManager,
+            Looper looper,
+            int displayId,
+            LightSensorControllerConfig config) {
+        this(config, new RealInjector(sensorManager, displayId), new LightSensorHandler(looper));
+    }
+
+    @VisibleForTesting
+    LightSensorController(
+            LightSensorControllerConfig config,
+            Injector injector,
+            Handler handler) {
+        if (config.mNormalLightSensorRate <= 0) {
+            throw new IllegalArgumentException("lightSensorRate must be above 0");
+        }
+        mInjector = injector;
+        int bufferInitialCapacity = (int) Math.ceil(
+                config.mAmbientLightHorizonLong * BUFFER_SLACK / config.mNormalLightSensorRate);
+        mClock = injector.getClock();
+        mHandler = handler;
+        mAmbientLightRingBuffer = new AmbientLightRingBuffer(bufferInitialCapacity, mClock);
+        mConfig = config;
+        mLightSensor = mInjector.getLightSensor(mConfig);
+        mTag = mInjector.getTag();
+    }
+
+    public void setListener(LightSensorListener listener) {
+        mListener = listener;
+    }
+
+    /**
+     * @return true if sensor registered, false if sensor already registered
+     */
+    public boolean enableLightSensorIfNeeded() {
+        if (!mLightSensorEnabled) {
+            mLightSensorEnabled = true;
+            mLightSensorEnableTime = mClock.uptimeMillis();
+            mCurrentLightSensorRate = mConfig.mInitialLightSensorRate;
+            mInjector.registerLightSensorListener(
+                    mLightSensorListener, mLightSensor, mCurrentLightSensorRate, mHandler);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return true if sensor unregistered, false if sensor already unregistered
+     */
+    public boolean disableLightSensorIfNeeded() {
+        if (mLightSensorEnabled) {
+            mLightSensorEnabled = false;
+            mAmbientLuxValid = !mConfig.mResetAmbientLuxAfterWarmUpConfig;
+            if (!mAmbientLuxValid) {
+                mPreThresholdLux = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+            }
+            mRecentLightSamples = 0;
+            mAmbientLightRingBuffer.clear();
+            mCurrentLightSensorRate = -1;
+            mInjector.unregisterLightSensorListener(mLightSensorListener);
+            return true;
+        }
+        return false;
+    }
+
+    public void setLoggingEnabled(boolean loggingEnabled) {
+        mLoggingEnabled = loggingEnabled;
+    }
+
+    /**
+     * Updates BrightnessEvent with LightSensorController details
+     */
+    public void updateBrightnessEvent(BrightnessEvent brightnessEvent) {
+        brightnessEvent.setPreThresholdLux(mPreThresholdLux);
+    }
+
+    /**
+     * Print the object's debug information into the given stream.
+     */
+    public void dump(PrintWriter pw) {
+        pw.println("LightSensorController state:");
+        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
+        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
+        pw.println("  mCurrentLightSensorRate=" + mCurrentLightSensorRate);
+        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
+        pw.println("  mAmbientLux=" + mAmbientLux);
+        pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
+        pw.println("  mPreThresholdLux=" + mPreThresholdLux);
+        pw.println("  mLastObservedLux=" + mLastObservedLux);
+        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
+        pw.println("  mSlowAmbientLux=" + mSlowAmbientLux);
+        pw.println("  mFastAmbientLux=" + mFastAmbientLux);
+        pw.println("  mIsIdleMode=" + mIsIdleMode);
+        pw.println("  mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold);
+        pw.println("  mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold);
+        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
+        pw.println("  mLightSensor=" + mLightSensor);
+        mConfig.dump(pw);
+    }
+
+    /**
+     * This method should be called when this LightSensorController is no longer in use
+     * i.e. when corresponding display removed
+     */
+    public void stop() {
+        mHandler.removeCallbacksAndMessages(null);
+        disableLightSensorIfNeeded();
+    }
+
+    public void setIdleMode(boolean isIdleMode) {
+        mIsIdleMode = isIdleMode;
+    }
+
+    /**
+     * returns true if LightSensorController holds valid ambient lux value
+     */
+    public boolean hasValidAmbientLux() {
+        return mAmbientLuxValid;
+    }
+
+    /**
+     * returns all last observed sensor values
+     */
+    public float[] getLastSensorValues() {
+        return mAmbientLightRingBuffer.getAllLuxValues();
+    }
+
+    /**
+     * returns all last observed sensor event timestamps
+     */
+    public long[] getLastSensorTimestamps() {
+        return mAmbientLightRingBuffer.getAllTimestamps();
+    }
+
+    public float getLastObservedLux() {
+        return mLastObservedLux;
+    }
+
+    private void handleLightSensorEvent(long time, float lux) {
+        Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux);
+        mHandler.removeCallbacks(mAmbientLuxUpdater);
+
+        if (mAmbientLightRingBuffer.size() == 0) {
+            // switch to using the steady-state sample rate after grabbing the initial light sample
+            adjustLightSensorRate(mConfig.mNormalLightSensorRate);
+        }
+        applyLightSensorMeasurement(time, lux);
+        updateAmbientLux(time);
+    }
+
+    private void applyLightSensorMeasurement(long time, float lux) {
+        mRecentLightSamples++;
+        mAmbientLightRingBuffer.prune(time - mConfig.mAmbientLightHorizonLong);
+        mAmbientLightRingBuffer.push(time, lux);
+        // Remember this sample value.
+        mLastObservedLux = lux;
+        mLastObservedLuxTime = time;
+    }
+
+    private void adjustLightSensorRate(int lightSensorRate) {
+        // if the light sensor rate changed, update the sensor listener
+        if (lightSensorRate != mCurrentLightSensorRate) {
+            if (mLoggingEnabled) {
+                Slog.d(mTag, "adjustLightSensorRate: "
+                        + "previousRate=" + mCurrentLightSensorRate + ", "
+                        + "currentRate=" + lightSensorRate);
+            }
+            mCurrentLightSensorRate = lightSensorRate;
+            mInjector.unregisterLightSensorListener(mLightSensorListener);
+            mInjector.registerLightSensorListener(
+                    mLightSensorListener, mLightSensor, lightSensorRate, mHandler);
+        }
+    }
+
+    private void setAmbientLux(float lux) {
+        if (mLoggingEnabled) {
+            Slog.d(mTag, "setAmbientLux(" + lux + ")");
+        }
+        if (lux < 0) {
+            Slog.w(mTag, "Ambient lux was negative, ignoring and setting to 0");
+            lux = 0;
+        }
+        mAmbientLux = lux;
+
+        if (mIsIdleMode) {
+            mAmbientBrighteningThreshold =
+                    mConfig.mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(lux);
+            mAmbientDarkeningThreshold =
+                    mConfig.mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(lux);
+        } else {
+            mAmbientBrighteningThreshold =
+                    mConfig.mAmbientBrightnessThresholds.getBrighteningThreshold(lux);
+            mAmbientDarkeningThreshold =
+                    mConfig.mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
+        }
+
+        mListener.onAmbientLuxChange(mAmbientLux);
+    }
+
+    private float calculateAmbientLux(long now, long horizon) {
+        if (mLoggingEnabled) {
+            Slog.d(mTag, "calculateAmbientLux(" + now + ", " + horizon + ")");
+        }
+        final int size = mAmbientLightRingBuffer.size();
+        if (size == 0) {
+            Slog.e(mTag, "calculateAmbientLux: No ambient light readings available");
+            return -1;
+        }
+
+        // Find the first measurement that is just outside of the horizon.
+        int endIndex = 0;
+        final long horizonStartTime = now - horizon;
+        for (int i = 0; i < size - 1; i++) {
+            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
+                endIndex++;
+            } else {
+                break;
+            }
+        }
+        if (mLoggingEnabled) {
+            Slog.d(mTag, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=("
+                    + mAmbientLightRingBuffer.getTime(endIndex) + ", "
+                    + mAmbientLightRingBuffer.getLux(endIndex) + ")");
+        }
+        float sum = 0;
+        float totalWeight = 0;
+        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
+        for (int i = size - 1; i >= endIndex; i--) {
+            long eventTime = mAmbientLightRingBuffer.getTime(i);
+            if (i == endIndex && eventTime < horizonStartTime) {
+                // If we're at the final value, make sure we only consider the part of the sample
+                // within our desired horizon.
+                eventTime = horizonStartTime;
+            }
+            final long startTime = eventTime - now;
+            float weight = calculateWeight(startTime, endTime);
+            float lux = mAmbientLightRingBuffer.getLux(i);
+            if (mLoggingEnabled) {
+                Slog.d(mTag, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: "
+                        + "lux=" + lux + ", "
+                        + "weight=" + weight);
+            }
+            totalWeight += weight;
+            sum += lux * weight;
+            endTime = startTime;
+        }
+        if (mLoggingEnabled) {
+            Slog.d(mTag, "calculateAmbientLux: "
+                    + "totalWeight=" + totalWeight + ", "
+                    + "newAmbientLux=" + (sum / totalWeight));
+        }
+        return sum / totalWeight;
+    }
+
+    private float calculateWeight(long startDelta, long endDelta) {
+        return weightIntegral(endDelta) - weightIntegral(startDelta);
+    }
+
+    // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the
+    // horizon we're looking at and provides a non-linear weighting for light samples.
+    private float weightIntegral(long x) {
+        return x * (x * 0.5f + mConfig.mWeightingIntercept);
+    }
+
+    private long nextAmbientLightBrighteningTransition(long time) {
+        final int size = mAmbientLightRingBuffer.size();
+        long earliestValidTime = time;
+        for (int i = size - 1; i >= 0; i--) {
+            if (mAmbientLightRingBuffer.getLux(i) <= mAmbientBrighteningThreshold) {
+                break;
+            }
+            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
+        }
+        return earliestValidTime + (mIsIdleMode ? mConfig.mBrighteningLightDebounceConfigIdle
+                : mConfig.mBrighteningLightDebounceConfig);
+    }
+
+    private long nextAmbientLightDarkeningTransition(long time) {
+        final int size = mAmbientLightRingBuffer.size();
+        long earliestValidTime = time;
+        for (int i = size - 1; i >= 0; i--) {
+            if (mAmbientLightRingBuffer.getLux(i) >= mAmbientDarkeningThreshold) {
+                break;
+            }
+            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
+        }
+        return earliestValidTime + (mIsIdleMode ? mConfig.mDarkeningLightDebounceConfigIdle
+                : mConfig.mDarkeningLightDebounceConfig);
+    }
+
+    private void updateAmbientLux() {
+        long time = mClock.uptimeMillis();
+        mAmbientLightRingBuffer.prune(time - mConfig.mAmbientLightHorizonLong);
+        updateAmbientLux(time);
+    }
+
+    private void updateAmbientLux(long time) {
+        // If the light sensor was just turned on then immediately update our initial
+        // estimate of the current ambient light level.
+        if (!mAmbientLuxValid) {
+            final long timeWhenSensorWarmedUp =
+                    mConfig.mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
+            if (time < timeWhenSensorWarmedUp) {
+                if (mLoggingEnabled) {
+                    Slog.d(mTag, "updateAmbientLux: Sensor not ready yet: "
+                            + "time=" + time + ", "
+                            + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
+                }
+                mHandler.postAtTime(mAmbientLuxUpdater, timeWhenSensorWarmedUp);
+                return;
+            }
+            mAmbientLuxValid = true;
+            setAmbientLux(calculateAmbientLux(time, mConfig.mAmbientLightHorizonShort));
+            if (mLoggingEnabled) {
+                Slog.d(mTag, "updateAmbientLux: Initializing: "
+                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
+                        + "mAmbientLux=" + mAmbientLux);
+            }
+        }
+
+        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
+        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
+        // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term
+        // change in lighting conditions, and a fast ambient lux to determine what the new
+        // brightness situation is since the slow lux can be quite slow to converge.
+        //
+        // Note that both values need to be checked for sufficient change before updating the
+        // proposed ambient light value since the slow value might be sufficiently far enough away
+        // from the fast value to cause a recalculation while its actually just converging on
+        // the fast value still.
+        mSlowAmbientLux = calculateAmbientLux(time, mConfig.mAmbientLightHorizonLong);
+        mFastAmbientLux = calculateAmbientLux(time, mConfig.mAmbientLightHorizonShort);
+
+        if ((mSlowAmbientLux >= mAmbientBrighteningThreshold
+                && mFastAmbientLux >= mAmbientBrighteningThreshold
+                && nextBrightenTransition <= time)
+                || (mSlowAmbientLux <= mAmbientDarkeningThreshold
+                && mFastAmbientLux <= mAmbientDarkeningThreshold
+                && nextDarkenTransition <= time)) {
+            mPreThresholdLux = mAmbientLux;
+            setAmbientLux(mFastAmbientLux);
+            if (mLoggingEnabled) {
+                Slog.d(mTag, "updateAmbientLux: "
+                        + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
+                        + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", "
+                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
+                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
+                        + "mAmbientLux=" + mAmbientLux);
+            }
+            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
+            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
+        }
+        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
+        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
+        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
+        // now. Rather than continually checking to see whether the weighted lux exceeds the
+        // threshold, schedule an update for when we'd normally expect another light sample, which
+        // should be enough time to decide whether we should actually transition to the new
+        // weighted ambient lux or not.
+        nextTransitionTime = nextTransitionTime > time ? nextTransitionTime
+                : time + mConfig.mNormalLightSensorRate;
+        if (mLoggingEnabled) {
+            Slog.d(mTag, "updateAmbientLux: Scheduling ambient lux update for "
+                    + nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
+        }
+        mHandler.postAtTime(mAmbientLuxUpdater, nextTransitionTime);
+    }
+
+    public interface LightSensorListener {
+        /**
+         * Called when new ambient lux value is ready
+         */
+        void onAmbientLuxChange(float ambientLux);
+    }
+
+    private static final class LightSensorHandler extends Handler {
+        private LightSensorHandler(Looper looper) {
+            super(looper, /* callback= */ null, /* async= */ true);
+        }
+    }
+
+    /**
+     * A ring buffer of ambient light measurements sorted by time.
+     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
+     * from oldest to newest.
+     */
+    @VisibleForTesting
+    static final class AmbientLightRingBuffer {
+
+        private float[] mRingLux;
+        private long[] mRingTime;
+        private int mCapacity;
+
+        // The first valid element and the next open slot.
+        // Note that if mCount is zero then there are no valid elements.
+        private int mStart;
+        private int mEnd;
+        private int mCount;
+
+        private final Clock mClock;
+
+        @VisibleForTesting
+        AmbientLightRingBuffer(int initialCapacity, Clock clock) {
+            mCapacity = initialCapacity;
+            mRingLux = new float[mCapacity];
+            mRingTime = new long[mCapacity];
+            mClock = clock;
+
+        }
+
+        @VisibleForTesting
+        float getLux(int index) {
+            return mRingLux[offsetOf(index)];
+        }
+
+        @VisibleForTesting
+        float[] getAllLuxValues() {
+            float[] values = new float[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingLux, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
+        @VisibleForTesting
+        long getTime(int index) {
+            return mRingTime[offsetOf(index)];
+        }
+
+        @VisibleForTesting
+        long[] getAllTimestamps() {
+            long[] values = new long[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingTime, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
+        @VisibleForTesting
+        void push(long time, float lux) {
+            int next = mEnd;
+            if (mCount == mCapacity) {
+                int newSize = mCapacity * 2;
+
+                float[] newRingLux = new float[newSize];
+                long[] newRingTime = new long[newSize];
+                int length = mCapacity - mStart;
+                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
+                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
+                if (mStart != 0) {
+                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
+                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
+                }
+                mRingLux = newRingLux;
+                mRingTime = newRingTime;
+
+                next = mCapacity;
+                mCapacity = newSize;
+                mStart = 0;
+            }
+            mRingTime[next] = time;
+            mRingLux[next] = lux;
+            mEnd = next + 1;
+            if (mEnd == mCapacity) {
+                mEnd = 0;
+            }
+            mCount++;
+        }
+
+        @VisibleForTesting
+        void prune(long horizon) {
+            if (mCount == 0) {
+                return;
+            }
+
+            while (mCount > 1) {
+                int next = mStart + 1;
+                if (next >= mCapacity) {
+                    next -= mCapacity;
+                }
+                if (mRingTime[next] > horizon) {
+                    // Some light sensors only produce data upon a change in the ambient light
+                    // levels, so we need to consider the previous measurement as the ambient light
+                    // level for all points in time up until we receive a new measurement. Thus, we
+                    // always want to keep the youngest element that would be removed from the
+                    // buffer and just set its measurement time to the horizon time since at that
+                    // point it is the ambient light level, and to remove it would be to drop a
+                    // valid data point within our horizon.
+                    break;
+                }
+                mStart = next;
+                mCount -= 1;
+            }
+
+            if (mRingTime[mStart] < horizon) {
+                mRingTime[mStart] = horizon;
+            }
+        }
+
+        @VisibleForTesting
+        int size() {
+            return mCount;
+        }
+
+        @VisibleForTesting
+        void clear() {
+            mStart = 0;
+            mEnd = 0;
+            mCount = 0;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buf = new StringBuilder();
+            buf.append('[');
+            for (int i = 0; i < mCount; i++) {
+                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
+                if (i != 0) {
+                    buf.append(", ");
+                }
+                buf.append(getLux(i));
+                buf.append(" / ");
+                buf.append(next - getTime(i));
+                buf.append("ms");
+            }
+            buf.append(']');
+            return buf.toString();
+        }
+
+        private int offsetOf(int index) {
+            if (index >= mCount || index < 0) {
+                throw new ArrayIndexOutOfBoundsException(index);
+            }
+            index += mStart;
+            if (index >= mCapacity) {
+                index -= mCapacity;
+            }
+            return index;
+        }
+    }
+
+    @VisibleForTesting
+    interface Injector {
+        Clock getClock();
+
+        Sensor getLightSensor(LightSensorControllerConfig config);
+
+        boolean registerLightSensorListener(
+                SensorEventListener listener, Sensor sensor, int rate, Handler handler);
+
+        void unregisterLightSensorListener(SensorEventListener listener);
+
+        String getTag();
+
+    }
+
+    private static class RealInjector implements Injector {
+        private final SensorManager mSensorManager;
+        private final int mSensorFallbackType;
+
+        private final String mTag;
+
+        private RealInjector(SensorManager sensorManager, int displayId) {
+            mSensorManager = sensorManager;
+            mSensorFallbackType = displayId == Display.DEFAULT_DISPLAY
+                    ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
+            mTag = "LightSensorController [" + displayId + "]";
+        }
+
+        @Override
+        public Clock getClock() {
+            return Clock.SYSTEM_CLOCK;
+        }
+
+        @Override
+        public Sensor getLightSensor(LightSensorControllerConfig config) {
+            return SensorUtils.findSensor(
+                    mSensorManager, config.mAmbientLightSensor, mSensorFallbackType);
+        }
+
+        @Override
+        public boolean registerLightSensorListener(
+                SensorEventListener listener, Sensor sensor, int rate, Handler handler) {
+            return mSensorManager.registerListener(listener, sensor, rate * 1000, handler);
+        }
+
+        @Override
+        public void unregisterLightSensorListener(SensorEventListener listener) {
+            mSensorManager.unregisterListener(listener);
+        }
+
+        @Override
+        public String getTag() {
+            return mTag;
+        }
+    }
+
+    public static class LightSensorControllerConfig {
+        // Steady-state light sensor event rate in milliseconds.
+        private final int mNormalLightSensorRate;
+        private final int mInitialLightSensorRate;
+
+        // If true immediately after the screen is turned on the controller will try to adjust the
+        // brightness based on the current sensor reads. If false, the controller will collect
+        // more data
+        // and only then decide whether to change brightness.
+        private final boolean mResetAmbientLuxAfterWarmUpConfig;
+
+        // Period of time in which to consider light samples for a short/long-term estimate of
+        // ambient
+        // light in milliseconds.
+        private final int mAmbientLightHorizonShort;
+        private final int mAmbientLightHorizonLong;
+
+
+        // Amount of time to delay auto-brightness after screen on while waiting for
+        // the light sensor to warm-up in milliseconds.
+        // May be 0 if no warm-up is required.
+        private final int mLightSensorWarmUpTimeConfig;
+
+
+        // The intercept used for the weighting calculation. This is used in order to keep all
+        // possible
+        // weighting values positive.
+        private final int mWeightingIntercept;
+
+        // Configuration object for determining thresholds to change brightness dynamically
+        private final HysteresisLevels mAmbientBrightnessThresholds;
+        private final HysteresisLevels mAmbientBrightnessThresholdsIdle;
+
+
+        // Stability requirements in milliseconds for accepting a new brightness level.  This is
+        // used
+        // for debouncing the light sensor.  Different constants are used to debounce the light
+        // sensor
+        // when adapting to brighter or darker environments.  This parameter controls how quickly
+        // brightness changes occur in response to an observed change in light level that exceeds
+        // the
+        // hysteresis threshold.
+        private final long mBrighteningLightDebounceConfig;
+        private final long mDarkeningLightDebounceConfig;
+        private final long mBrighteningLightDebounceConfigIdle;
+        private final long mDarkeningLightDebounceConfigIdle;
+
+        private final SensorData mAmbientLightSensor;
+
+        @VisibleForTesting
+        LightSensorControllerConfig(int initialLightSensorRate, int normalLightSensorRate,
+                boolean resetAmbientLuxAfterWarmUpConfig, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, int lightSensorWarmUpTimeConfig,
+                int weightingIntercept, HysteresisLevels ambientBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+                SensorData ambientLightSensor) {
+            mInitialLightSensorRate = initialLightSensorRate;
+            mNormalLightSensorRate = normalLightSensorRate;
+            mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
+            mAmbientLightHorizonShort = ambientLightHorizonShort;
+            mAmbientLightHorizonLong = ambientLightHorizonLong;
+            mLightSensorWarmUpTimeConfig = lightSensorWarmUpTimeConfig;
+            mWeightingIntercept = weightingIntercept;
+            mAmbientBrightnessThresholds = ambientBrightnessThresholds;
+            mAmbientBrightnessThresholdsIdle = ambientBrightnessThresholdsIdle;
+            mBrighteningLightDebounceConfig = brighteningLightDebounceConfig;
+            mDarkeningLightDebounceConfig = darkeningLightDebounceConfig;
+            mBrighteningLightDebounceConfigIdle = brighteningLightDebounceConfigIdle;
+            mDarkeningLightDebounceConfigIdle = darkeningLightDebounceConfigIdle;
+            mAmbientLightSensor = ambientLightSensor;
+        }
+
+        private void dump(PrintWriter pw) {
+            pw.println("LightSensorControllerConfig:");
+            pw.println("  mInitialLightSensorRate=" + mInitialLightSensorRate);
+            pw.println("  mNormalLightSensorRate=" + mNormalLightSensorRate);
+            pw.println("  mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig);
+            pw.println("  mAmbientLightHorizonShort=" + mAmbientLightHorizonShort);
+            pw.println("  mAmbientLightHorizonLong=" + mAmbientLightHorizonLong);
+            pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
+            pw.println("  mWeightingIntercept=" + mWeightingIntercept);
+            pw.println("  mAmbientBrightnessThresholds=");
+            mAmbientBrightnessThresholds.dump(pw);
+            pw.println("  mAmbientBrightnessThresholdsIdle=");
+            mAmbientBrightnessThresholdsIdle.dump(pw);
+            pw.println("  mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig);
+            pw.println("  mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig);
+            pw.println(
+                    "  mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle);
+            pw.println("  mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle);
+            pw.println("  mAmbientLightSensor=" + mAmbientLightSensor);
+        }
+
+        /**
+         * Creates LightSensorControllerConfig object form Resources and DisplayDeviceConfig
+         */
+        public static LightSensorControllerConfig create(Resources res, DisplayDeviceConfig ddc) {
+            int lightSensorRate = res.getInteger(R.integer.config_autoBrightnessLightSensorRate);
+            int initialLightSensorRate = res.getInteger(
+                    R.integer.config_autoBrightnessInitialLightSensorRate);
+            if (initialLightSensorRate == -1) {
+                initialLightSensorRate = lightSensorRate;
+            } else if (initialLightSensorRate > lightSensorRate) {
+                Slog.w("LightSensorControllerConfig",
+                        "Expected config_autoBrightnessInitialLightSensorRate ("
+                                + initialLightSensorRate + ") to be less than or equal to "
+                                + "config_autoBrightnessLightSensorRate (" + lightSensorRate
+                                + ").");
+            }
+
+            boolean resetAmbientLuxAfterWarmUp = res.getBoolean(
+                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+            int lightSensorWarmUpTimeConfig = res.getInteger(
+                    R.integer.config_lightSensorWarmupTime);
+
+            return new LightSensorControllerConfig(initialLightSensorRate, lightSensorRate,
+                    resetAmbientLuxAfterWarmUp, ddc.getAmbientHorizonShort(),
+                    ddc.getAmbientHorizonLong(), lightSensorWarmUpTimeConfig,
+                    ddc.getAmbientHorizonLong(),
+                    HysteresisLevels.getAmbientBrightnessThresholds(ddc),
+                    HysteresisLevels.getAmbientBrightnessThresholdsIdle(ddc),
+                    ddc.getAutoBrightnessBrighteningLightDebounce(),
+                    ddc.getAutoBrightnessDarkeningLightDebounce(),
+                    ddc.getAutoBrightnessBrighteningLightDebounceIdle(),
+                    ddc.getAutoBrightnessDarkeningLightDebounceIdle(),
+                    ddc.getAmbientLightSensor()
+            );
+        }
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 54de64e..dd87572 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -22,9 +22,7 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyFloat;
@@ -38,9 +36,6 @@
 
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
-import android.hardware.Sensor;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
 import android.os.PowerManager;
@@ -52,6 +47,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.os.Clock;
+import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.testutils.OffsettableClock;
 
@@ -61,7 +58,6 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
@@ -69,31 +65,18 @@
 public class AutomaticBrightnessControllerTest {
     private static final float BRIGHTNESS_MIN_FLOAT = 0.0f;
     private static final float BRIGHTNESS_MAX_FLOAT = 1.0f;
-    private static final int LIGHT_SENSOR_RATE = 20;
     private static final int INITIAL_LIGHT_SENSOR_RATE = 20;
-    private static final int BRIGHTENING_LIGHT_DEBOUNCE_CONFIG = 2000;
-    private static final int DARKENING_LIGHT_DEBOUNCE_CONFIG = 4000;
-    private static final int BRIGHTENING_LIGHT_DEBOUNCE_CONFIG_IDLE = 1000;
-    private static final int DARKENING_LIGHT_DEBOUNCE_CONFIG_IDLE = 2000;
     private static final float DOZE_SCALE_FACTOR = 0.54f;
-    private static final boolean RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG = false;
-    private static final int LIGHT_SENSOR_WARMUP_TIME = 0;
-    private static final int AMBIENT_LIGHT_HORIZON_SHORT = 1000;
-    private static final int AMBIENT_LIGHT_HORIZON_LONG = 2000;
     private static final float EPSILON = 0.001f;
     private OffsettableClock mClock = new OffsettableClock();
     private TestLooper mTestLooper;
     private Context mContext;
     private AutomaticBrightnessController mController;
-    private Sensor mLightSensor;
 
-    @Mock SensorManager mSensorManager;
     @Mock BrightnessMappingStrategy mBrightnessMappingStrategy;
     @Mock BrightnessMappingStrategy mIdleBrightnessMappingStrategy;
     @Mock BrightnessMappingStrategy mDozeBrightnessMappingStrategy;
-    @Mock HysteresisLevels mAmbientBrightnessThresholds;
     @Mock HysteresisLevels mScreenBrightnessThresholds;
-    @Mock HysteresisLevels mAmbientBrightnessThresholdsIdle;
     @Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
     @Mock Handler mNoOpHandler;
     @Mock BrightnessRangeController mBrightnessRangeController;
@@ -101,17 +84,18 @@
     BrightnessClamperController mBrightnessClamperController;
     @Mock BrightnessThrottler mBrightnessThrottler;
 
+    @Mock
+    LightSensorController mLightSensorController;
+
     @Before
     public void setUp() throws Exception {
         // Share classloader to allow package private access.
         System.setProperty("dexmaker.share_classloader", "true");
         MockitoAnnotations.initMocks(this);
 
-        mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
         mContext = InstrumentationRegistry.getContext();
         setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ false,
-                /* useHorizon= */ true);
+                BrightnessMappingStrategy.INVALID_NITS);
     }
 
     @After
@@ -123,8 +107,7 @@
         }
     }
 
-    private void setupController(float userLux, float userNits, boolean applyDebounce,
-            boolean useHorizon) {
+    private void setupController(float userLux, float userNits) {
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
 
@@ -147,25 +130,22 @@
                     }
 
                     @Override
-                    AutomaticBrightnessController.Clock createClock() {
-                        return mClock::now;
+                    Clock createClock() {
+                        return new Clock() {
+                            @Override
+                            public long uptimeMillis() {
+                                return mClock.now();
+                            }
+                        };
                     }
 
                 }, // pass in test looper instead, pass in offsettable clock
-                () -> { }, mTestLooper.getLooper(), mSensorManager, mLightSensor,
-                brightnessMappingStrategyMap, LIGHT_SENSOR_WARMUP_TIME, BRIGHTNESS_MIN_FLOAT,
-                BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, LIGHT_SENSOR_RATE,
-                INITIAL_LIGHT_SENSOR_RATE, applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG : 0,
-                applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG : 0,
-                applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG_IDLE : 0,
-                applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG_IDLE : 0,
-                RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG,
-                mAmbientBrightnessThresholds, mScreenBrightnessThresholds,
-                mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
+                () -> { }, mTestLooper.getLooper(),
+                brightnessMappingStrategyMap, BRIGHTNESS_MIN_FLOAT,
+                BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, mScreenBrightnessThresholds,
+                mScreenBrightnessThresholdsIdle,
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
-                useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
-                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
-                mBrightnessClamperController
+                userLux, userNits, mLightSensorController, mBrightnessClamperController
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
@@ -186,20 +166,15 @@
 
     @Test
     public void testNoHysteresisAtMinBrightness() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.02f as a brightness value
         float lux1 = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness1 = 0.02f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
-                .thenReturn(lux1);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
-                .thenReturn(lux1);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux1), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness1);
 
@@ -210,39 +185,31 @@
                 .thenReturn(1.0f);
 
         // Send new sensor value and verify
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
+        listener.onAmbientLuxChange(lux1);
         assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
         // Set up system to return 0.0f (minimum possible brightness) as a brightness value
         float lux2 = 10.0f;
         float normalizedBrightness2 = 0.0f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
-                .thenReturn(lux2);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux2))
-                .thenReturn(lux2);
         when(mBrightnessMappingStrategy.getBrightness(anyFloat(), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness2);
 
         // Send new sensor value and verify
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
+        listener.onAmbientLuxChange(lux2);
         assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
     public void testNoHysteresisAtMaxBrightness() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.98f as a brightness value
         float lux1 = 100.0f;
         float normalizedBrightness1 = 0.98f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
-                .thenReturn(lux1);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
-                .thenReturn(lux1);
+
         when(mBrightnessMappingStrategy.getBrightness(eq(lux1), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness1);
 
@@ -253,35 +220,30 @@
                 .thenReturn(1.1f);
 
         // Send new sensor value and verify
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
+        listener.onAmbientLuxChange(lux1);
         assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
 
         // Set up system to return 1.0f as a brightness value (brightness_max)
         float lux2 = 110.0f;
         float normalizedBrightness2 = 1.0f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
-                .thenReturn(lux2);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux2))
-                .thenReturn(lux2);
         when(mBrightnessMappingStrategy.getBrightness(anyFloat(), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness2);
 
         // Send new sensor value and verify
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
+        listener.onAmbientLuxChange(lux2);
         assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
     public void testUserAddUserDataPoint() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
+        listener.onAmbientLuxChange(1000);
 
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -298,12 +260,11 @@
     public void testRecalculateSplines() throws Exception {
         // Enabling the light sensor, and setting the ambient lux to 1000
         int currentLux = 1000;
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, currentLux));
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        listener.onAmbientLuxChange(currentLux);
 
         // User sets brightness to 0.5f
         when(mBrightnessMappingStrategy.getBrightness(currentLux,
@@ -333,14 +294,13 @@
 
     @Test
     public void testShortTermModelTimesOut() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
+        listener.onAmbientLuxChange(123);
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -354,7 +314,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
+        listener.onAmbientLuxChange(1000);
         mTestLooper.moveTimeForward(
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
@@ -373,14 +333,13 @@
 
     @Test
     public void testShortTermModelDoesntTimeOut() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
+        listener.onAmbientLuxChange(123);
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
                 0.51f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
@@ -399,7 +358,7 @@
         mTestLooper.dispatchAll();
 
         // Sensor reads 100000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 678910));
+        listener.onAmbientLuxChange(678910);
         mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
 
         // Verify short term model is not reset.
@@ -413,14 +372,13 @@
 
     @Test
     public void testShortTermModelIsRestoredWhenSwitchingWithinTimeout() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
+        listener.onAmbientLuxChange(123);
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -440,7 +398,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
+        listener.onAmbientLuxChange(1000);
         mTestLooper.moveTimeForward(
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
@@ -459,14 +417,13 @@
 
     @Test
     public void testShortTermModelNotRestoredAfterTimeout() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
+        listener.onAmbientLuxChange(123);
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -488,7 +445,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
+        listener.onAmbientLuxChange(1000);
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
@@ -506,14 +463,13 @@
 
     @Test
     public void testSwitchBetweenModesNoUserInteractions() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
+        listener.onAmbientLuxChange(123);
         when(mBrightnessMappingStrategy.getShortTermModelTimeout()).thenReturn(2000L);
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(
                 PowerManager.BRIGHTNESS_INVALID_FLOAT);
@@ -529,7 +485,7 @@
                 BrightnessMappingStrategy.INVALID_LUX);
 
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
+        listener.onAmbientLuxChange(1000);
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
@@ -545,14 +501,19 @@
 
     @Test
     public void testSwitchToIdleMappingStrategy() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        clearInvocations(mBrightnessMappingStrategy);
 
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
+        listener.onAmbientLuxChange(1000);
+
+
+        verify(mBrightnessMappingStrategy).getBrightness(anyFloat(), any(), anyInt());
+
+        clearInvocations(mBrightnessMappingStrategy);
 
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -561,22 +522,19 @@
                 /* shouldResetShortTermModel= */ true);
 
         // There should be a user data point added to the mapper.
-        verify(mBrightnessMappingStrategy, times(1)).addUserDataPoint(/* lux= */ 1000f,
+        verify(mBrightnessMappingStrategy).addUserDataPoint(/* lux= */ 1000f,
                 /* brightness= */ 0.5f);
-        verify(mBrightnessMappingStrategy, times(2)).setBrightnessConfiguration(any());
-        verify(mBrightnessMappingStrategy, times(3)).getBrightness(anyFloat(), any(), anyInt());
+        verify(mBrightnessMappingStrategy).setBrightnessConfiguration(any());
+        verify(mBrightnessMappingStrategy).getBrightness(anyFloat(), any(), anyInt());
 
+        clearInvocations(mBrightnessMappingStrategy);
         // Now let's do the same for idle mode
         mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        // Called once when switching,
-        // setAmbientLux() is called twice and once in updateAutoBrightness(),
-        // nextAmbientLightBrighteningTransition() and nextAmbientLightDarkeningTransition() are
-        // called twice each.
-        verify(mBrightnessMappingStrategy, times(8)).getMode();
-        // Called when switching.
-        verify(mBrightnessMappingStrategy, times(1)).getShortTermModelTimeout();
-        verify(mBrightnessMappingStrategy, times(1)).getUserBrightness();
-        verify(mBrightnessMappingStrategy, times(1)).getUserLux();
+
+        verify(mBrightnessMappingStrategy).getMode();
+        verify(mBrightnessMappingStrategy).getShortTermModelTimeout();
+        verify(mBrightnessMappingStrategy).getUserBrightness();
+        verify(mBrightnessMappingStrategy).getUserLux();
 
         // Ensure, after switching, original BMS is not used anymore
         verifyNoMoreInteractions(mBrightnessMappingStrategy);
@@ -588,154 +546,25 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Ensure we use the correct mapping strategy
-        verify(mIdleBrightnessMappingStrategy, times(1)).addUserDataPoint(/* lux= */ 1000f,
+        verify(mIdleBrightnessMappingStrategy).addUserDataPoint(/* lux= */ 1000f,
                 /* brightness= */ 0.5f);
     }
 
     @Test
-    public void testAmbientLightHorizon() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        long increment = 500;
-        // set autobrightness to low
-        // t = 0
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-
-        // t = 500
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-
-        // t = 1000
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
-
-        // t = 1500
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
-
-        // t = 2000
-        // ensure that our reading is at 0.
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
-
-        // t = 2500
-        // first 10000 lux sensor event reading
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
-        assertTrue(mController.getAmbientLux() > 0.0f);
-        assertTrue(mController.getAmbientLux() < 10000.0f);
-
-        // t = 3000
-        // lux reading should still not yet be 10000.
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
-        assertTrue(mController.getAmbientLux() > 0.0f);
-        assertTrue(mController.getAmbientLux() < 10000.0f);
-
-        // t = 3500
-        mClock.fastForward(increment);
-        // lux has been high (10000) for 1000ms.
-        // lux reading should be 10000
-        // short horizon (ambient lux) is high, long horizon is still not high
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
-        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
-
-        // t = 4000
-        // stay high
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
-        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
-
-        // t = 4500
-        Mockito.clearInvocations(mBrightnessMappingStrategy);
-        mClock.fastForward(increment);
-        // short horizon is high, long horizon is high too
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
-        verify(mBrightnessMappingStrategy, times(1)).getBrightness(10000, null, -1);
-        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
-
-        // t = 5000
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-        assertTrue(mController.getAmbientLux() > 0.0f);
-        assertTrue(mController.getAmbientLux() < 10000.0f);
-
-        // t = 5500
-        mClock.fastForward(increment);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-        assertTrue(mController.getAmbientLux() > 0.0f);
-        assertTrue(mController.getAmbientLux() < 10000.0f);
-
-        // t = 6000
-        mClock.fastForward(increment);
-        // ambient lux goes to 0
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
-        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
-
-        // only the values within the horizon should be kept
-        assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(),
-                EPSILON);
-        assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000},
-                mController.getLastSensorTimestamps());
-    }
-
-    @Test
-    public void testHysteresisLevels() {
-        float[] ambientBrighteningThresholds = {50, 100};
-        float[] ambientDarkeningThresholds = {10, 20};
-        float[] ambientThresholdLevels = {0, 500};
-        float ambientDarkeningMinChangeThreshold = 3.0f;
-        float ambientBrighteningMinChangeThreshold = 1.5f;
-        HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds,
-                ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels,
-                ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold);
-
-        // test low, activate minimum change thresholds.
-        assertEquals(1.5f, hysteresisLevels.getBrighteningThreshold(0.0f), EPSILON);
-        assertEquals(0f, hysteresisLevels.getDarkeningThreshold(0.0f), EPSILON);
-        assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON);
-
-        // test max
-        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
-        assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2);
-        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
-
-        // test just below threshold
-        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
-        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
-
-        // test at (considered above) threshold
-        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
-        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
-    }
-
-    @Test
     public void testBrightnessGetsThrottled() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Set up system to return max brightness at 100 lux
         final float normalizedBrightness = BRIGHTNESS_MAX_FLOAT;
         final float lux = 100.0f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux))
-                .thenReturn(lux);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux))
-                .thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness);
 
         // Sensor reads 100 lux. We should get max brightness.
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
+        listener.onAmbientLuxChange(lux);
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getAutomaticScreenBrightness(), 0.0f);
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getRawAutomaticScreenBrightness(), 0.0f);
 
@@ -763,94 +592,6 @@
     }
 
     @Test
-    public void testGetSensorReadings() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
-        int increment = 11;
-        int lux = 5000;
-        for (int i = 0; i < 1000; i++) {
-            lux += increment;
-            mClock.fastForward(increment);
-            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
-        }
-
-        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1);
-        float[] sensorValues = mController.getLastSensorValues();
-        long[] sensorTimestamps = mController.getLastSensorTimestamps();
-
-        // Only the values within the horizon should be kept
-        assertEquals(valuesCount, sensorValues.length);
-        assertEquals(valuesCount, sensorTimestamps.length);
-
-        long sensorTimestamp = mClock.now();
-        for (int i = valuesCount - 1; i >= 1; i--) {
-            assertEquals(lux, sensorValues[i], EPSILON);
-            assertEquals(sensorTimestamp, sensorTimestamps[i]);
-            lux -= increment;
-            sensorTimestamp -= increment;
-        }
-        assertEquals(lux, sensorValues[0], EPSILON);
-        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
-    }
-
-    @Test
-    public void testGetSensorReadingsFullBuffer() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-        int initialCapacity = 150;
-
-        // Choose values such that the ring buffer is pruned
-        int increment1 = 200;
-        int lux = 5000;
-        for (int i = 0; i < 20; i++) {
-            lux += increment1;
-            mClock.fastForward(increment1);
-            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
-        }
-
-        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1);
-
-        // Choose values such that the buffer becomes full
-        int increment2 = 1;
-        for (int i = 0; i < initialCapacity - valuesCount; i++) {
-            lux += increment2;
-            mClock.fastForward(increment2);
-            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
-        }
-
-        float[] sensorValues = mController.getLastSensorValues();
-        long[] sensorTimestamps = mController.getLastSensorTimestamps();
-
-        // The buffer should be full
-        assertEquals(initialCapacity, sensorValues.length);
-        assertEquals(initialCapacity, sensorTimestamps.length);
-
-        long sensorTimestamp = mClock.now();
-        for (int i = initialCapacity - 1; i >= 1; i--) {
-            assertEquals(lux, sensorValues[i], EPSILON);
-            assertEquals(sensorTimestamp, sensorTimestamps[i]);
-
-            if (i >= valuesCount) {
-                lux -= increment2;
-                sensorTimestamp -= increment2;
-            } else {
-                lux -= increment1;
-                sensorTimestamp -= increment1;
-            }
-        }
-        assertEquals(lux, sensorValues[0], EPSILON);
-        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
-    }
-
-    @Test
     public void testResetShortTermModelWhenConfigChanges() {
         when(mBrightnessMappingStrategy.setBrightnessConfiguration(any())).thenReturn(true);
 
@@ -875,179 +616,22 @@
         float userNits = 500;
         float userBrightness = 0.3f;
         when(mBrightnessMappingStrategy.getBrightnessFromNits(userNits)).thenReturn(userBrightness);
-        setupController(userLux, userNits, /* applyDebounce= */ true,
-                /* useHorizon= */ false);
+        setupController(userLux, userNits);
         verify(mBrightnessMappingStrategy).addUserDataPoint(userLux, userBrightness);
     }
 
     @Test
-    public void testBrighteningLightDebounce() throws Exception {
-        clearInvocations(mSensorManager);
-        setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
-                /* useHorizon= */ false);
-
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        // t = 0
-        // Initial lux
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-
-        // t = 1000
-        // Lux isn't steady yet
-        mClock.fastForward(1000);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-
-        // t = 1500
-        // Lux isn't steady yet
-        mClock.fastForward(500);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-
-        // t = 2500
-        // Lux is steady now
-        mClock.fastForward(1000);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-    }
-
-    @Test
-    public void testDarkeningLightDebounce() throws Exception {
-        clearInvocations(mSensorManager);
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(anyFloat()))
-                .thenReturn(10000f);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(anyFloat()))
-                .thenReturn(10000f);
-        setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
-                /* useHorizon= */ false);
-
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        // t = 0
-        // Initial lux
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-
-        // t = 2000
-        // Lux isn't steady yet
-        mClock.fastForward(2000);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-
-        // t = 2500
-        // Lux isn't steady yet
-        mClock.fastForward(500);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-
-        // t = 4500
-        // Lux is steady now
-        mClock.fastForward(2000);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-    }
-
-    @Test
-    public void testBrighteningLightDebounceIdle() throws Exception {
-        clearInvocations(mSensorManager);
-        setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
-                /* useHorizon= */ false);
-
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        // t = 0
-        // Initial lux
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-
-        // t = 500
-        // Lux isn't steady yet
-        mClock.fastForward(500);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-
-        // t = 1500
-        // Lux is steady now
-        mClock.fastForward(1000);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-    }
-
-    @Test
-    public void testDarkeningLightDebounceIdle() throws Exception {
-        clearInvocations(mSensorManager);
-        when(mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(anyFloat()))
-                .thenReturn(10000f);
-        when(mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(anyFloat()))
-                .thenReturn(10000f);
-        setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
-                /* useHorizon= */ false);
-
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        // t = 0
-        // Initial lux
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-
-        // t = 1000
-        // Lux isn't steady yet
-        mClock.fastForward(1000);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(1200, mController.getAmbientLux(), EPSILON);
-
-        // t = 2500
-        // Lux is steady now
-        mClock.fastForward(1500);
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
-        assertEquals(500, mController.getAmbientLux(), EPSILON);
-    }
-
-    @Test
     public void testBrightnessBasedOnLastObservedLux() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
+        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
 
         // Send a new sensor value, disable the sensor and verify
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
         mController.configure(AUTO_BRIGHTNESS_DISABLED, /* configuration= */ null,
                 /* brightness= */ 0, /* userChangedBrightness= */ false, /* adjustment= */ 0,
                 /* userChanged= */ false, DisplayPowerRequest.POLICY_BRIGHT, Display.STATE_ON,
@@ -1059,21 +643,19 @@
 
     @Test
     public void testAutoBrightnessInDoze() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
+        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -1082,7 +664,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
+        listener.onAmbientLuxChange(lux);
 
         // The brightness should be scaled by the doze factor
         assertEquals(normalizedBrightness * DOZE_SCALE_FACTOR,
@@ -1095,21 +677,19 @@
 
     @Test
     public void testAutoBrightnessInDoze_ShouldNotScaleIfUsingDozeCurve() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mDozeBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
+        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Switch mode to DOZE
         mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
@@ -1121,7 +701,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
+        listener.onAmbientLuxChange(lux);
 
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
@@ -1133,21 +713,19 @@
 
     @Test
     public void testAutoBrightnessInDoze_ShouldNotScaleIfScreenOn() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
+                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
+        verify(mLightSensorController).setListener(listenerCaptor.capture());
+        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
+        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -1156,7 +734,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
+        listener.onAmbientLuxChange(lux);
 
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 740ffc9..a4d1f5c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -28,7 +28,6 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
@@ -79,6 +78,8 @@
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.LightSensorController;
+import com.android.server.display.brightness.TestUtilsKt;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
@@ -1161,30 +1162,19 @@
                 any(AutomaticBrightnessController.Callbacks.class),
                 any(Looper.class),
                 eq(mSensorManagerMock),
-                /* lightSensor= */ any(),
                 /* brightnessMappingStrategyMap= */ any(SparseArray.class),
-                /* lightSensorWarmUpTime= */ anyInt(),
                 /* brightnessMin= */ anyFloat(),
                 /* brightnessMax= */ anyFloat(),
                 /* dozeScaleFactor */ anyFloat(),
-                /* lightSensorRate= */ anyInt(),
-                /* initialLightSensorRate= */ anyInt(),
-                /* brighteningLightDebounceConfig */ anyLong(),
-                /* darkeningLightDebounceConfig */ anyLong(),
-                /* brighteningLightDebounceConfigIdle= */ anyLong(),
-                /* darkeningLightDebounceConfigIdle= */ anyLong(),
-                /* resetAmbientLuxAfterWarmUpConfig= */ anyBoolean(),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
                 any(HysteresisLevels.class),
                 any(HysteresisLevels.class),
                 eq(mContext),
                 any(BrightnessRangeController.class),
                 any(BrightnessThrottler.class),
-                /* ambientLightHorizonShort= */ anyInt(),
-                /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
                 eq(nits),
+                eq(DISPLAY_ID),
+                any(LightSensorController.LightSensorControllerConfig.class),
                 any(BrightnessClamperController.class)
         );
     }
@@ -2107,22 +2097,22 @@
         }
 
         @Override
+        LightSensorController.LightSensorControllerConfig getLightSensorControllerConfig(
+                Context context, DisplayDeviceConfig displayDeviceConfig) {
+            return TestUtilsKt.createLightSensorControllerConfig();
+        }
+
+        @Override
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager, Sensor lightSensor,
+                SensorManager sensorManager,
                 SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                boolean resetAmbientLuxAfterWarmUpConfig,
-                HysteresisLevels ambientBrightnessThresholds,
+                float brightnessMin, float brightnessMax, float dozeScaleFactor,
                 HysteresisLevels screenBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits,
+                BrightnessThrottler brightnessThrottler, float userLux, float userNits,
+                int displayId, LightSensorController.LightSensorControllerConfig config,
                 BrightnessClamperController brightnessClamperController) {
             return mAutomaticBrightnessController;
         }
@@ -2135,18 +2125,12 @@
         }
 
         @Override
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold) {
+        HysteresisLevels getBrightnessThresholdsIdleHysteresisLevels(DisplayDeviceConfig ddc) {
             return mHysteresisLevels;
         }
 
         @Override
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+        HysteresisLevels getBrightnessThresholdsHysteresisLevels(DisplayDeviceConfig ddc) {
             return mHysteresisLevels;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt b/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt
new file mode 100644
index 0000000..02d6946
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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.server.display
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.brightness.createHysteresisLevels
+import kotlin.test.assertEquals
+import org.junit.Test
+
+private const val FLOAT_TOLERANCE = 0.001f
+@SmallTest
+class HysteresisLevelsTest {
+    @Test
+    fun `test hysteresis levels`() {
+        val hysteresisLevels = createHysteresisLevels(
+            brighteningThresholdsPercentages = floatArrayOf(50f, 100f),
+            darkeningThresholdsPercentages = floatArrayOf(10f, 20f),
+            brighteningThresholdLevels = floatArrayOf(0f, 500f),
+            darkeningThresholdLevels = floatArrayOf(0f, 500f),
+            minDarkeningThreshold = 3f,
+            minBrighteningThreshold = 1.5f
+        )
+
+        // test low, activate minimum change thresholds.
+        assertEquals(1.5f, hysteresisLevels.getBrighteningThreshold(0.0f), FLOAT_TOLERANCE)
+        assertEquals(0f, hysteresisLevels.getDarkeningThreshold(0.0f), FLOAT_TOLERANCE)
+        assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), FLOAT_TOLERANCE)
+
+        // test max
+        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
+        assertEquals(
+            20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), FLOAT_TOLERANCE * 2)
+        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), FLOAT_TOLERANCE)
+
+        // test just below threshold
+        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), FLOAT_TOLERANCE)
+        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), FLOAT_TOLERANCE)
+
+        // test at (considered above) threshold
+        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), FLOAT_TOLERANCE)
+        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), FLOAT_TOLERANCE)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt
new file mode 100644
index 0000000..5fe9178
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 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.server.display.brightness
+
+import androidx.test.filters.SmallTest
+import com.android.internal.os.Clock
+import com.android.server.display.brightness.LightSensorController.AmbientLightRingBuffer
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+
+private const val BUFFER_INITIAL_CAPACITY = 3
+
+@SmallTest
+class AmbientLightRingBufferTest {
+
+    private val buffer = AmbientLightRingBuffer(BUFFER_INITIAL_CAPACITY, mock<Clock>())
+
+    @Test
+    fun `test created empty`() {
+        assertThat(buffer.size()).isEqualTo(0)
+    }
+
+    @Test
+    fun `test push to empty buffer`() {
+        buffer.push(1000, 0.5f)
+
+        assertThat(buffer.size()).isEqualTo(1)
+        assertThat(buffer.getLux(0)).isEqualTo(0.5f)
+        assertThat(buffer.getTime(0)).isEqualTo(1000)
+    }
+
+    @Test
+    fun `test prune keeps youngest outside horizon and sets time to horizon`() {
+        buffer.push(1000, 0.5f)
+        buffer.push(2000, 0.6f)
+        buffer.push(3000, 0.7f)
+
+        buffer.prune(2500)
+
+        assertThat(buffer.size()).isEqualTo(2)
+
+        assertThat(buffer.getLux(0)).isEqualTo(0.6f)
+        assertThat(buffer.getTime(0)).isEqualTo(2500)
+    }
+
+    @Test
+    fun `test prune keeps inside horizon`() {
+        buffer.push(1000, 0.5f)
+        buffer.push(2000, 0.6f)
+        buffer.push(3000, 0.7f)
+
+        buffer.prune(2500)
+
+        assertThat(buffer.size()).isEqualTo(2)
+
+        assertThat(buffer.getLux(1)).isEqualTo(0.7f)
+        assertThat(buffer.getTime(1)).isEqualTo(3000)
+    }
+
+
+    @Test
+    fun `test pushes correctly after prune`() {
+        buffer.push(1000, 0.5f)
+        buffer.push(2000, 0.6f)
+        buffer.push(3000, 0.7f)
+        buffer.prune(2500)
+
+        buffer.push(4000, 0.8f)
+
+        assertThat(buffer.size()).isEqualTo(3)
+
+        assertThat(buffer.getLux(0)).isEqualTo(0.6f)
+        assertThat(buffer.getTime(0)).isEqualTo(2500)
+        assertThat(buffer.getLux(1)).isEqualTo(0.7f)
+        assertThat(buffer.getTime(1)).isEqualTo(3000)
+        assertThat(buffer.getLux(2)).isEqualTo(0.8f)
+        assertThat(buffer.getTime(2)).isEqualTo(4000)
+    }
+
+    @Test
+    fun `test increase buffer size`() {
+        buffer.push(1000, 0.5f)
+        buffer.push(2000, 0.6f)
+        buffer.push(3000, 0.7f)
+
+        buffer.push(4000, 0.8f)
+
+        assertThat(buffer.size()).isEqualTo(4)
+
+        assertThat(buffer.getLux(0)).isEqualTo(0.5f)
+        assertThat(buffer.getTime(0)).isEqualTo(1000)
+        assertThat(buffer.getLux(1)).isEqualTo(0.6f)
+        assertThat(buffer.getTime(1)).isEqualTo(2000)
+        assertThat(buffer.getLux(2)).isEqualTo(0.7f)
+        assertThat(buffer.getTime(2)).isEqualTo(3000)
+        assertThat(buffer.getLux(3)).isEqualTo(0.8f)
+        assertThat(buffer.getTime(3)).isEqualTo(4000)
+    }
+
+    @Test
+    fun `test buffer clear`() {
+        buffer.push(1000, 0.5f)
+        buffer.push(2000, 0.6f)
+        buffer.push(3000, 0.7f)
+
+        buffer.clear()
+
+        assertThat(buffer.size()).isEqualTo(0)
+        assertThrows(ArrayIndexOutOfBoundsException::class.java) {
+            buffer.getLux(0)
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt
new file mode 100644
index 0000000..966134a
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2024 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.server.display.brightness
+
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.os.Handler
+import androidx.test.filters.SmallTest
+import com.android.internal.os.Clock
+import com.android.server.display.TestUtils
+import com.android.server.display.brightness.LightSensorController.Injector
+import com.android.server.display.brightness.LightSensorController.LightSensorControllerConfig
+import com.android.server.testutils.OffsettableClock
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.ceil
+import kotlin.test.assertEquals
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+private const val FLOAT_TOLERANCE = 0.001f
+
+@SmallTest
+class LightSensorControllerTest {
+
+    private val testHandler = TestHandler(null)
+    private val testInjector = TestInjector()
+
+    @Test
+    fun `test ambient light horizon`() {
+        val lightSensorController = LightSensorController(
+            createLightSensorControllerConfig(
+                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
+                brighteningLightDebounceConfig = 0,
+                darkeningLightDebounceConfig = 0,
+                ambientLightHorizonShort = 1000,
+                ambientLightHorizonLong = 2000
+            ), testInjector, testHandler)
+
+        var reportedAmbientLux = 0f
+        lightSensorController.setListener { lux ->
+            reportedAmbientLux = lux
+        }
+        lightSensorController.enableLightSensorIfNeeded()
+
+        assertThat(testInjector.sensorEventListener).isNotNull()
+
+        val timeIncrement = 500L
+        // set ambient lux to low
+        // t = 0
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+
+        // t = 500
+        //
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+
+        // t = 1000
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t = 1500
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t = 2000
+        // ensure that our reading is at 0.
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t = 2500
+        // first 10000 lux sensor event reading
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
+        assertTrue(reportedAmbientLux > 0f)
+        assertTrue(reportedAmbientLux < 10_000f)
+
+        // t = 3000
+        // lux reading should still not yet be 10000.
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
+        assertTrue(reportedAmbientLux > 0)
+        assertTrue(reportedAmbientLux < 10_000f)
+
+        // t = 3500
+        testInjector.clock.fastForward(timeIncrement)
+        // at short horizon,  first value outside will be used in calculation (t = 2000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
+        assertTrue(reportedAmbientLux > 0f)
+        assertTrue(reportedAmbientLux < 10_000f)
+
+        // t = 4000
+        // lux has been high (10000) for more than 1000ms.
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
+        assertEquals(10_000f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t = 4500
+        testInjector.clock.fastForward(timeIncrement)
+        // short horizon is high, long horizon is high too
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
+        assertEquals(10_000f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t = 5000
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertTrue(reportedAmbientLux > 0f)
+        assertTrue(reportedAmbientLux < 10_000f)
+
+        // t = 5500
+        testInjector.clock.fastForward(timeIncrement)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertTrue(reportedAmbientLux > 0f)
+        assertTrue(reportedAmbientLux < 10_000f)
+
+        // t = 6000
+        testInjector.clock.fastForward(timeIncrement)
+        // at short horizon, first value outside will be used in calculation (t = 4500)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertTrue(reportedAmbientLux > 0f)
+        assertTrue(reportedAmbientLux < 10_000f)
+
+        // t = 6500
+        testInjector.clock.fastForward(timeIncrement)
+        // ambient lux goes to 0
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
+        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // only the values within the horizon should be kept
+        assertArrayEquals(floatArrayOf(10_000f, 0f, 0f, 0f, 0f),
+            lightSensorController.lastSensorValues, FLOAT_TOLERANCE)
+        assertArrayEquals(longArrayOf(4_500, 5_000, 5_500, 6_000, 6_500),
+            lightSensorController.lastSensorTimestamps)
+    }
+
+    @Test
+    fun `test brightening debounce`() {
+        val lightSensorController = LightSensorController(
+            createLightSensorControllerConfig(
+                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
+                brighteningLightDebounceConfig = 1500,
+                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
+                ambientLightHorizonLong = 10_000,
+                // brightening threshold is set to previous lux value
+                ambientBrightnessThresholds = createHysteresisLevels(
+                    brighteningThresholdLevels = floatArrayOf(),
+                    brighteningThresholdsPercentages = floatArrayOf(),
+                    minBrighteningThreshold = 0f
+                )
+            ), testInjector, testHandler)
+        lightSensorController.setIdleMode(false)
+
+        var reportedAmbientLux = 0f
+        lightSensorController.setListener { lux ->
+            reportedAmbientLux = lux
+        }
+        lightSensorController.enableLightSensorIfNeeded()
+
+        assertThat(testInjector.sensorEventListener).isNotNull()
+
+        // t0 (0)
+        // Initial lux, initial brightening threshold
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t1 (1000)
+        // Lux increase, first brightening event
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1800f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t2 (2000) (t2 - t1 < brighteningLightDebounceConfig)
+        // Lux increase, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2000f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t3 (3000) (t3 - t1 < brighteningLightDebounceConfig)
+        // Lux increase, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2200f))
+        assertEquals(2200f, reportedAmbientLux, FLOAT_TOLERANCE)
+    }
+
+    @Test
+    fun `test sensor readings`() {
+        val ambientLightHorizonLong = 2_500
+        val lightSensorController = LightSensorController(
+            createLightSensorControllerConfig(
+                ambientLightHorizonLong = ambientLightHorizonLong
+            ), testInjector, testHandler)
+        lightSensorController.setListener { }
+        lightSensorController.setIdleMode(false)
+        lightSensorController.enableLightSensorIfNeeded()
+
+        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
+        val increment = 11
+        var lux = 5000
+        for (i in 0 until 1000) {
+            lux += increment
+            testInjector.clock.fastForward(increment.toLong())
+            testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(lux.toFloat()))
+        }
+
+        val valuesCount = ceil(ambientLightHorizonLong.toDouble() / increment + 1).toInt()
+        val sensorValues = lightSensorController.lastSensorValues
+        val sensorTimestamps = lightSensorController.lastSensorTimestamps
+
+        // Only the values within the horizon should be kept
+        assertEquals(valuesCount, sensorValues.size)
+        assertEquals(valuesCount, sensorTimestamps.size)
+
+        var sensorTimestamp = testInjector.clock.now()
+        for (i in valuesCount - 1 downTo 1) {
+            assertEquals(lux.toFloat(), sensorValues[i], FLOAT_TOLERANCE)
+            assertEquals(sensorTimestamp, sensorTimestamps[i])
+            lux -= increment
+            sensorTimestamp -= increment
+        }
+        assertEquals(lux.toFloat(), sensorValues[0], FLOAT_TOLERANCE)
+        assertEquals(testInjector.clock.now() - ambientLightHorizonLong, sensorTimestamps[0])
+    }
+
+    @Test
+    fun `test darkening debounce`() {
+        val lightSensorController = LightSensorController(
+            createLightSensorControllerConfig(
+                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
+                darkeningLightDebounceConfig = 1500,
+                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
+                ambientLightHorizonLong = 10_000,
+                // darkening threshold is set to previous lux value
+                ambientBrightnessThresholds = createHysteresisLevels(
+                    darkeningThresholdLevels = floatArrayOf(),
+                    darkeningThresholdsPercentages = floatArrayOf(),
+                    minDarkeningThreshold = 0f
+                )
+            ), testInjector, testHandler)
+
+        lightSensorController.setIdleMode(false)
+
+        var reportedAmbientLux = 0f
+        lightSensorController.setListener { lux ->
+            reportedAmbientLux = lux
+        }
+        lightSensorController.enableLightSensorIfNeeded()
+
+        assertThat(testInjector.sensorEventListener).isNotNull()
+
+        // t0 (0)
+        // Initial lux, initial darkening threshold
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t1 (1000)
+        // Lux decreased, first darkening event
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(800f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t2 (2000) (t2 - t1 < darkeningLightDebounceConfig)
+        // Lux decreased, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(500f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t3 (3000) (t3 - t1 < darkeningLightDebounceConfig)
+        // Lux decreased, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(400f))
+        assertEquals(400f, reportedAmbientLux, FLOAT_TOLERANCE)
+    }
+
+    @Test
+    fun `test brightening debounce in idle mode`() {
+        val lightSensorController = LightSensorController(
+            createLightSensorControllerConfig(
+                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
+                brighteningLightDebounceConfigIdle = 1500,
+                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
+                ambientLightHorizonLong = 10_000,
+                // brightening threshold is set to previous lux value
+                ambientBrightnessThresholdsIdle = createHysteresisLevels(
+                    brighteningThresholdLevels = floatArrayOf(),
+                    brighteningThresholdsPercentages = floatArrayOf(),
+                    minBrighteningThreshold = 0f
+                )
+            ), testInjector, testHandler)
+        lightSensorController.setIdleMode(true)
+
+        var reportedAmbientLux = 0f
+        lightSensorController.setListener { lux ->
+            reportedAmbientLux = lux
+        }
+        lightSensorController.enableLightSensorIfNeeded()
+
+        assertThat(testInjector.sensorEventListener).isNotNull()
+
+        // t0 (0)
+        // Initial lux, initial brightening threshold
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t1 (1000)
+        // Lux increase, first brightening event
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1800f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t2 (2000) (t2 - t1 < brighteningLightDebounceConfigIdle)
+        // Lux increase, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2000f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t3 (3000) (t3 - t1 < brighteningLightDebounceConfigIdle)
+        // Lux increase, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2200f))
+        assertEquals(2200f, reportedAmbientLux, FLOAT_TOLERANCE)
+    }
+
+    @Test
+    fun `test darkening debounce in idle mode`() {
+        val lightSensorController = LightSensorController(
+            createLightSensorControllerConfig(
+                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
+                darkeningLightDebounceConfigIdle = 1500,
+                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
+                ambientLightHorizonLong = 10_000,
+                // darkening threshold is set to previous lux value
+                ambientBrightnessThresholdsIdle = createHysteresisLevels(
+                    darkeningThresholdLevels = floatArrayOf(),
+                    darkeningThresholdsPercentages = floatArrayOf(),
+                    minDarkeningThreshold = 0f
+                )
+            ), testInjector, testHandler)
+
+        lightSensorController.setIdleMode(true)
+
+        var reportedAmbientLux = 0f
+        lightSensorController.setListener { lux ->
+            reportedAmbientLux = lux
+        }
+        lightSensorController.enableLightSensorIfNeeded()
+
+        assertThat(testInjector.sensorEventListener).isNotNull()
+
+        // t0 (0)
+        // Initial lux, initial darkening threshold
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t1 (1000)
+        // Lux decreased, first darkening event
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(800f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t2 (2000) (t2 - t1 < darkeningLightDebounceConfigIdle)
+        // Lux decreased, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(500f))
+        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
+
+        // t3 (3000) (t3 - t1 < darkeningLightDebounceConfigIdle)
+        // Lux decreased, but isn't steady yet
+        testInjector.clock.fastForward(1000)
+        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(400f))
+        assertEquals(400f, reportedAmbientLux, FLOAT_TOLERANCE)
+    }
+
+
+    private fun sensorEvent(value: Float) = SensorEvent(
+        testInjector.testSensor, 0, 0, floatArrayOf(value)
+    )
+
+    private class TestInjector : Injector {
+        val testSensor: Sensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor")
+        val clock: OffsettableClock = OffsettableClock.Stopped()
+
+        var sensorEventListener: SensorEventListener? = null
+        override fun getClock(): Clock {
+            return object : Clock() {
+                override fun uptimeMillis(): Long {
+                    return clock.now()
+                }
+            }
+        }
+
+        override fun getLightSensor(config: LightSensorControllerConfig): Sensor {
+            return testSensor
+        }
+
+        override fun registerLightSensorListener(
+            listener: SensorEventListener,
+            sensor: Sensor,
+            rate: Int,
+            handler: Handler
+        ): Boolean {
+            sensorEventListener = listener
+            return true
+        }
+
+        override fun unregisterLightSensorListener(listener: SensorEventListener) {
+            sensorEventListener = null
+        }
+
+        override fun getTag(): String {
+            return "LightSensorControllerTest"
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt
new file mode 100644
index 0000000..1328f5f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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.server.display.brightness
+
+import com.android.server.display.HysteresisLevels
+import com.android.server.display.config.SensorData
+
+@JvmOverloads
+fun createLightSensorControllerConfig(
+    initialSensorRate: Int = 1,
+    normalSensorRate: Int = 2,
+    resetAmbientLuxAfterWarmUpConfig: Boolean = true,
+    ambientLightHorizonShort: Int = 1,
+    ambientLightHorizonLong: Int = 10_000,
+    lightSensorWarmUpTimeConfig: Int = 0,
+    weightingIntercept: Int = 10_000,
+    ambientBrightnessThresholds: HysteresisLevels = createHysteresisLevels(),
+    ambientBrightnessThresholdsIdle: HysteresisLevels = createHysteresisLevels(),
+    brighteningLightDebounceConfig: Long = 100_000,
+    darkeningLightDebounceConfig: Long = 100_000,
+    brighteningLightDebounceConfigIdle: Long = 100_000,
+    darkeningLightDebounceConfigIdle: Long = 100_000,
+    ambientLightSensor: SensorData = SensorData()
+) = LightSensorController.LightSensorControllerConfig(
+    initialSensorRate,
+    normalSensorRate,
+    resetAmbientLuxAfterWarmUpConfig,
+    ambientLightHorizonShort,
+    ambientLightHorizonLong,
+    lightSensorWarmUpTimeConfig,
+    weightingIntercept,
+    ambientBrightnessThresholds,
+    ambientBrightnessThresholdsIdle,
+    brighteningLightDebounceConfig,
+    darkeningLightDebounceConfig,
+    brighteningLightDebounceConfigIdle,
+    darkeningLightDebounceConfigIdle,
+    ambientLightSensor
+)
+
+fun createHysteresisLevels(
+    brighteningThresholdsPercentages: FloatArray = floatArrayOf(),
+    darkeningThresholdsPercentages: FloatArray = floatArrayOf(),
+    brighteningThresholdLevels: FloatArray = floatArrayOf(),
+    darkeningThresholdLevels: FloatArray = floatArrayOf(),
+    minDarkeningThreshold: Float = 0f,
+    minBrighteningThreshold: Float = 0f,
+    potentialOldBrightnessRange: Boolean = false
+) = HysteresisLevels(
+    brighteningThresholdsPercentages,
+    darkeningThresholdsPercentages,
+    brighteningThresholdLevels,
+    darkeningThresholdLevels,
+    minDarkeningThreshold,
+    minBrighteningThreshold,
+    potentialOldBrightnessRange
+)
\ No newline at end of file