1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.display;
18
19import com.android.server.LocalServices;
20import com.android.server.twilight.TwilightListener;
21import com.android.server.twilight.TwilightManager;
22import com.android.server.twilight.TwilightState;
23
24import android.hardware.Sensor;
25import android.hardware.SensorEvent;
26import android.hardware.SensorEventListener;
27import android.hardware.SensorManager;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.Message;
31import android.os.PowerManager;
32import android.os.SystemClock;
33import android.text.format.DateUtils;
34import android.util.MathUtils;
35import android.util.Spline;
36import android.util.Slog;
37import android.util.TimeUtils;
38
39import java.io.PrintWriter;
40import java.util.Arrays;
41
42class AutomaticBrightnessController {
43    private static final String TAG = "AutomaticBrightnessController";
44
45    private static final boolean DEBUG = false;
46    private static final boolean DEBUG_PRETEND_LIGHT_SENSOR_ABSENT = false;
47
48    // If true, enables the use of the screen auto-brightness adjustment setting.
49    private static final boolean USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT = true;
50
51    // The maximum range of gamma adjustment possible using the screen
52    // auto-brightness adjustment setting.
53    private static final float SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT_MAX_GAMMA = 3.0f;
54
55    // Light sensor event rate in milliseconds.
56    private static final int LIGHT_SENSOR_RATE_MILLIS = 1000;
57
58    // Period of time in which to consider light samples in milliseconds.
59    private static final int AMBIENT_LIGHT_HORIZON = 10000;
60
61    // Stability requirements in milliseconds for accepting a new brightness level.  This is used
62    // for debouncing the light sensor.  Different constants are used to debounce the light sensor
63    // when adapting to brighter or darker environments.  This parameter controls how quickly
64    // brightness changes occur in response to an observed change in light level that exceeds the
65    // hysteresis threshold.
66    private static final long BRIGHTENING_LIGHT_DEBOUNCE = 4000;
67    private static final long DARKENING_LIGHT_DEBOUNCE = 8000;
68
69    // Hysteresis constraints for brightening or darkening.
70    // The recent lux must have changed by at least this fraction relative to the
71    // current ambient lux before a change will be considered.
72    private static final float BRIGHTENING_LIGHT_HYSTERESIS = 0.10f;
73    private static final float DARKENING_LIGHT_HYSTERESIS = 0.20f;
74
75    // The intercept used for the weighting calculation. This is used in order to keep all possible
76    // weighting values positive.
77    private static final int WEIGHTING_INTERCEPT = AMBIENT_LIGHT_HORIZON;
78
79    // How long the current sensor reading is assumed to be valid beyond the current time.
80    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
81    // non-zero, which in turn ensures that the total weight is non-zero.
82    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
83
84    // If true, enables the use of the current time as an auto-brightness adjustment.
85    // The basic idea here is to expand the dynamic range of auto-brightness
86    // when it is especially dark outside.  The light sensor tends to perform
87    // poorly at low light levels so we compensate for it by making an
88    // assumption about the environment.
89    private static final boolean USE_TWILIGHT_ADJUSTMENT =
90            PowerManager.useTwilightAdjustmentFeature();
91
92    // Specifies the maximum magnitude of the time of day adjustment.
93    private static final float TWILIGHT_ADJUSTMENT_MAX_GAMMA = 1.5f;
94
95    // The amount of time after or before sunrise over which to start adjusting
96    // the gamma.  We want the change to happen gradually so that it is below the
97    // threshold of perceptibility and so that the adjustment has maximum effect
98    // well after dusk.
99    private static final long TWILIGHT_ADJUSTMENT_TIME = DateUtils.HOUR_IN_MILLIS * 2;
100
101    private static final int MSG_UPDATE_AMBIENT_LUX = 1;
102
103    // Callbacks for requesting updates to the the display's power state
104    private final Callbacks mCallbacks;
105
106    // The sensor manager.
107    private final SensorManager mSensorManager;
108
109    // The light sensor, or null if not available or needed.
110    private final Sensor mLightSensor;
111
112    // The twilight service.
113    private final TwilightManager mTwilight;
114
115    // The auto-brightness spline adjustment.
116    // The brightness values have been scaled to a range of 0..1.
117    private final Spline mScreenAutoBrightnessSpline;
118
119    // The minimum and maximum screen brightnesses.
120    private final int mScreenBrightnessRangeMinimum;
121    private final int mScreenBrightnessRangeMaximum;
122    private final float mDozeScaleFactor;
123
124    // Amount of time to delay auto-brightness after screen on while waiting for
125    // the light sensor to warm-up in milliseconds.
126    // May be 0 if no warm-up is required.
127    private int mLightSensorWarmUpTimeConfig;
128
129    // Set to true if the light sensor is enabled.
130    private boolean mLightSensorEnabled;
131
132    // The time when the light sensor was enabled.
133    private long mLightSensorEnableTime;
134
135    // The currently accepted nominal ambient light level.
136    private float mAmbientLux;
137
138    // True if mAmbientLux holds a valid value.
139    private boolean mAmbientLuxValid;
140
141    // The ambient light level threshold at which to brighten or darken the screen.
142    private float mBrighteningLuxThreshold;
143    private float mDarkeningLuxThreshold;
144
145    // The most recent light sample.
146    private float mLastObservedLux;
147
148    // The time of the most light recent sample.
149    private long mLastObservedLuxTime;
150
151    // The number of light samples collected since the light sensor was enabled.
152    private int mRecentLightSamples;
153
154    // A ring buffer containing all of the recent ambient light sensor readings.
155    private AmbientLightRingBuffer mAmbientLightRingBuffer;
156
157    // The handler
158    private AutomaticBrightnessHandler mHandler;
159
160    // The screen brightness level that has been chosen by the auto-brightness
161    // algorithm.  The actual brightness should ramp towards this value.
162    // We preserve this value even when we stop using the light sensor so
163    // that we can quickly revert to the previous auto-brightness level
164    // while the light sensor warms up.
165    // Use -1 if there is no current auto-brightness value available.
166    private int mScreenAutoBrightness = -1;
167
168    // The screen auto-brightness adjustment factor in the range -1 (dimmer) to 1 (brighter)
169    private float mScreenAutoBrightnessAdjustment = 0.0f;
170
171    // The last screen auto-brightness gamma.  (For printing in dump() only.)
172    private float mLastScreenAutoBrightnessGamma = 1.0f;
173
174    // Are we going to adjust brightness while dozing.
175    private boolean mDozing;
176
177    public AutomaticBrightnessController(Callbacks callbacks, Looper looper,
178            SensorManager sensorManager, Spline autoBrightnessSpline, int lightSensorWarmUpTime,
179            int brightnessMin, int brightnessMax, float dozeScaleFactor) {
180        mCallbacks = callbacks;
181        mTwilight = LocalServices.getService(TwilightManager.class);
182        mSensorManager = sensorManager;
183        mScreenAutoBrightnessSpline = autoBrightnessSpline;
184        mScreenBrightnessRangeMinimum = brightnessMin;
185        mScreenBrightnessRangeMaximum = brightnessMax;
186        mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
187        mDozeScaleFactor = dozeScaleFactor;
188
189        mHandler = new AutomaticBrightnessHandler(looper);
190        mAmbientLightRingBuffer = new AmbientLightRingBuffer();
191
192        if (!DEBUG_PRETEND_LIGHT_SENSOR_ABSENT) {
193            mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
194        }
195
196        if (USE_TWILIGHT_ADJUSTMENT) {
197            mTwilight.registerListener(mTwilightListener, mHandler);
198        }
199    }
200
201    public int getAutomaticScreenBrightness() {
202        if (mDozing) {
203            return (int) (mScreenAutoBrightness * mDozeScaleFactor);
204        }
205        return mScreenAutoBrightness;
206    }
207
208    public void configure(boolean enable, float adjustment, boolean dozing) {
209        // While dozing, the application processor may be suspended which will prevent us from
210        // receiving new information from the light sensor. On some devices, we may be able to
211        // switch to a wake-up light sensor instead but for now we will simply disable the sensor
212        // and hold onto the last computed screen auto brightness.  We save the dozing flag for
213        // debugging purposes.
214        mDozing = dozing;
215        boolean changed = setLightSensorEnabled(enable && !dozing);
216        changed |= setScreenAutoBrightnessAdjustment(adjustment);
217        if (changed) {
218            updateAutoBrightness(false /*sendUpdate*/);
219        }
220    }
221
222    public void dump(PrintWriter pw) {
223        pw.println();
224        pw.println("Automatic Brightness Controller Configuration:");
225        pw.println("  mScreenAutoBrightnessSpline=" + mScreenAutoBrightnessSpline);
226        pw.println("  mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
227        pw.println("  mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
228        pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
229
230        pw.println();
231        pw.println("Automatic Brightness Controller State:");
232        pw.println("  mLightSensor=" + mLightSensor);
233        pw.println("  mTwilight.getCurrentState()=" + mTwilight.getCurrentState());
234        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
235        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
236        pw.println("  mAmbientLux=" + mAmbientLux);
237        pw.println("  mBrighteningLuxThreshold=" + mBrighteningLuxThreshold);
238        pw.println("  mDarkeningLuxThreshold=" + mDarkeningLuxThreshold);
239        pw.println("  mLastObservedLux=" + mLastObservedLux);
240        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
241        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
242        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
243        pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
244        pw.println("  mScreenAutoBrightnessAdjustment=" + mScreenAutoBrightnessAdjustment);
245        pw.println("  mLastScreenAutoBrightnessGamma=" + mLastScreenAutoBrightnessGamma);
246        pw.println("  mDozing=" + mDozing);
247    }
248
249    private boolean setLightSensorEnabled(boolean enable) {
250        if (enable) {
251            if (!mLightSensorEnabled) {
252                mLightSensorEnabled = true;
253                mLightSensorEnableTime = SystemClock.uptimeMillis();
254                mSensorManager.registerListener(mLightSensorListener, mLightSensor,
255                        LIGHT_SENSOR_RATE_MILLIS * 1000, mHandler);
256                return true;
257            }
258        } else {
259            if (mLightSensorEnabled) {
260                mLightSensorEnabled = false;
261                mAmbientLuxValid = false;
262                mRecentLightSamples = 0;
263                mAmbientLightRingBuffer.clear();
264                mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
265                mSensorManager.unregisterListener(mLightSensorListener);
266            }
267        }
268        return false;
269    }
270
271    private void handleLightSensorEvent(long time, float lux) {
272        mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
273
274        applyLightSensorMeasurement(time, lux);
275        updateAmbientLux(time);
276    }
277
278    private void applyLightSensorMeasurement(long time, float lux) {
279        mRecentLightSamples++;
280        mAmbientLightRingBuffer.prune(time - AMBIENT_LIGHT_HORIZON);
281        mAmbientLightRingBuffer.push(time, lux);
282
283        // Remember this sample value.
284        mLastObservedLux = lux;
285        mLastObservedLuxTime = time;
286    }
287
288    private boolean setScreenAutoBrightnessAdjustment(float adjustment) {
289        if (adjustment != mScreenAutoBrightnessAdjustment) {
290            mScreenAutoBrightnessAdjustment = adjustment;
291            return true;
292        }
293        return false;
294    }
295
296    private void setAmbientLux(float lux) {
297        mAmbientLux = lux;
298        mBrighteningLuxThreshold = mAmbientLux * (1.0f + BRIGHTENING_LIGHT_HYSTERESIS);
299        mDarkeningLuxThreshold = mAmbientLux * (1.0f - DARKENING_LIGHT_HYSTERESIS);
300    }
301
302    private float calculateAmbientLux(long now) {
303        final int N = mAmbientLightRingBuffer.size();
304        if (N == 0) {
305            Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
306            return -1;
307        }
308        float sum = 0;
309        float totalWeight = 0;
310        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
311        for (int i = N - 1; i >= 0; i--) {
312            long startTime = (mAmbientLightRingBuffer.getTime(i) - now);
313            float weight = calculateWeight(startTime, endTime);
314            float lux = mAmbientLightRingBuffer.getLux(i);
315            if (DEBUG) {
316                Slog.d(TAG, "calculateAmbientLux: [" +
317                        (startTime) + ", " +
318                        (endTime) + "]: lux=" + lux + ", weight=" + weight);
319            }
320            totalWeight += weight;
321            sum += mAmbientLightRingBuffer.getLux(i) * weight;
322            endTime = startTime;
323        }
324        if (DEBUG) {
325            Slog.d(TAG, "calculateAmbientLux: totalWeight=" + totalWeight +
326                    ", newAmbientLux=" + (sum / totalWeight));
327        }
328        return sum / totalWeight;
329    }
330
331    private static float calculateWeight(long startDelta, long endDelta) {
332        return weightIntegral(endDelta) - weightIntegral(startDelta);
333    }
334
335    // Evaluates the integral of y = x + WEIGHTING_INTERCEPT. This is always positive for the
336    // horizon we're looking at and provides a non-linear weighting for light samples.
337    private static float weightIntegral(long x) {
338        return x * (x * 0.5f + WEIGHTING_INTERCEPT);
339    }
340
341    private long nextAmbientLightBrighteningTransition(long time) {
342        final int N = mAmbientLightRingBuffer.size();
343        long earliestValidTime = time;
344        for (int i = N - 1; i >= 0; i--) {
345            if (mAmbientLightRingBuffer.getLux(i) <= mBrighteningLuxThreshold) {
346                break;
347            }
348            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
349        }
350        return earliestValidTime + BRIGHTENING_LIGHT_DEBOUNCE;
351    }
352
353    private long nextAmbientLightDarkeningTransition(long time) {
354        final int N = mAmbientLightRingBuffer.size();
355        long earliestValidTime = time;
356        for (int i = N - 1; i >= 0; i--) {
357            if (mAmbientLightRingBuffer.getLux(i) >= mDarkeningLuxThreshold) {
358                break;
359            }
360            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
361        }
362        return earliestValidTime + DARKENING_LIGHT_DEBOUNCE;
363    }
364
365    private void updateAmbientLux() {
366        long time = SystemClock.uptimeMillis();
367        mAmbientLightRingBuffer.prune(time - AMBIENT_LIGHT_HORIZON);
368        updateAmbientLux(time);
369    }
370
371    private void updateAmbientLux(long time) {
372        // If the light sensor was just turned on then immediately update our initial
373        // estimate of the current ambient light level.
374        if (!mAmbientLuxValid) {
375            final long timeWhenSensorWarmedUp =
376                mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
377            if (time < timeWhenSensorWarmedUp) {
378                if (DEBUG) {
379                    Slog.d(TAG, "updateAmbientLux: Sensor not  ready yet: "
380                            + "time=" + time
381                            + ", timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
382                }
383                mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
384                        timeWhenSensorWarmedUp);
385                return;
386            }
387            setAmbientLux(calculateAmbientLux(time));
388            mAmbientLuxValid = true;
389            if (DEBUG) {
390                Slog.d(TAG, "updateAmbientLux: Initializing: "
391                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer
392                        + ", mAmbientLux=" + mAmbientLux);
393            }
394            updateAutoBrightness(true);
395        }
396
397        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
398        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
399        float ambientLux = calculateAmbientLux(time);
400
401        if (ambientLux >= mBrighteningLuxThreshold && nextBrightenTransition <= time
402                || ambientLux <= mDarkeningLuxThreshold && nextDarkenTransition <= time) {
403            setAmbientLux(ambientLux);
404            if (DEBUG) {
405                Slog.d(TAG, "updateAmbientLux: "
406                        + ((ambientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
407                        + "mBrighteningLuxThreshold=" + mBrighteningLuxThreshold
408                        + ", mAmbientLightRingBuffer=" + mAmbientLightRingBuffer
409                        + ", mAmbientLux=" + mAmbientLux);
410            }
411            updateAutoBrightness(true);
412            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
413            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
414        }
415        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
416        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
417        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
418        // now. Rather than continually checking to see whether the weighted lux exceeds the
419        // threshold, schedule an update for when we'd normally expect another light sample, which
420        // should be enough time to decide whether we should actually transition to the new
421        // weighted ambient lux or not.
422        nextTransitionTime =
423                nextTransitionTime > time ? nextTransitionTime : time + LIGHT_SENSOR_RATE_MILLIS;
424        if (DEBUG) {
425            Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for "
426                    + nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
427        }
428        mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
429    }
430
431    private void updateAutoBrightness(boolean sendUpdate) {
432        if (!mAmbientLuxValid) {
433            return;
434        }
435
436        float value = mScreenAutoBrightnessSpline.interpolate(mAmbientLux);
437        float gamma = 1.0f;
438
439        if (USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT
440                && mScreenAutoBrightnessAdjustment != 0.0f) {
441            final float adjGamma = MathUtils.pow(SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT_MAX_GAMMA,
442                    Math.min(1.0f, Math.max(-1.0f, -mScreenAutoBrightnessAdjustment)));
443            gamma *= adjGamma;
444            if (DEBUG) {
445                Slog.d(TAG, "updateAutoBrightness: adjGamma=" + adjGamma);
446            }
447        }
448
449        if (USE_TWILIGHT_ADJUSTMENT) {
450            TwilightState state = mTwilight.getCurrentState();
451            if (state != null && state.isNight()) {
452                final long now = System.currentTimeMillis();
453                final float earlyGamma =
454                        getTwilightGamma(now, state.getYesterdaySunset(), state.getTodaySunrise());
455                final float lateGamma =
456                        getTwilightGamma(now, state.getTodaySunset(), state.getTomorrowSunrise());
457                gamma *= earlyGamma * lateGamma;
458                if (DEBUG) {
459                    Slog.d(TAG, "updateAutoBrightness: earlyGamma=" + earlyGamma
460                            + ", lateGamma=" + lateGamma);
461                }
462            }
463        }
464
465        if (gamma != 1.0f) {
466            final float in = value;
467            value = MathUtils.pow(value, gamma);
468            if (DEBUG) {
469                Slog.d(TAG, "updateAutoBrightness: gamma=" + gamma
470                        + ", in=" + in + ", out=" + value);
471            }
472        }
473
474        int newScreenAutoBrightness =
475            clampScreenBrightness(Math.round(value * PowerManager.BRIGHTNESS_ON));
476        if (mScreenAutoBrightness != newScreenAutoBrightness) {
477            if (DEBUG) {
478                Slog.d(TAG, "updateAutoBrightness: mScreenAutoBrightness="
479                        + mScreenAutoBrightness + ", newScreenAutoBrightness="
480                        + newScreenAutoBrightness);
481            }
482
483            mScreenAutoBrightness = newScreenAutoBrightness;
484            mLastScreenAutoBrightnessGamma = gamma;
485            if (sendUpdate) {
486                mCallbacks.updateBrightness();
487            }
488        }
489    }
490
491    private int clampScreenBrightness(int value) {
492        return MathUtils.constrain(value,
493                mScreenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum);
494    }
495
496    private static float getTwilightGamma(long now, long lastSunset, long nextSunrise) {
497        if (lastSunset < 0 || nextSunrise < 0
498                || now < lastSunset || now > nextSunrise) {
499            return 1.0f;
500        }
501
502        if (now < lastSunset + TWILIGHT_ADJUSTMENT_TIME) {
503            return MathUtils.lerp(1.0f, TWILIGHT_ADJUSTMENT_MAX_GAMMA,
504                    (float)(now - lastSunset) / TWILIGHT_ADJUSTMENT_TIME);
505        }
506
507        if (now > nextSunrise - TWILIGHT_ADJUSTMENT_TIME) {
508            return MathUtils.lerp(1.0f, TWILIGHT_ADJUSTMENT_MAX_GAMMA,
509                    (float)(nextSunrise - now) / TWILIGHT_ADJUSTMENT_TIME);
510        }
511
512        return TWILIGHT_ADJUSTMENT_MAX_GAMMA;
513    }
514
515    private final class AutomaticBrightnessHandler extends Handler {
516        public AutomaticBrightnessHandler(Looper looper) {
517            super(looper, null, true /*async*/);
518        }
519
520        @Override
521        public void handleMessage(Message msg) {
522            switch (msg.what) {
523                case MSG_UPDATE_AMBIENT_LUX:
524                    updateAmbientLux();
525                    break;
526            }
527        }
528    }
529
530    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
531        @Override
532        public void onSensorChanged(SensorEvent event) {
533            if (mLightSensorEnabled) {
534                final long time = SystemClock.uptimeMillis();
535                final float lux = event.values[0];
536                handleLightSensorEvent(time, lux);
537            }
538        }
539
540        @Override
541        public void onAccuracyChanged(Sensor sensor, int accuracy) {
542            // Not used.
543        }
544    };
545
546    private final TwilightListener mTwilightListener = new TwilightListener() {
547        @Override
548        public void onTwilightStateChanged() {
549            updateAutoBrightness(true /*sendUpdate*/);
550        }
551    };
552
553    /** Callbacks to request updates to the display's power state. */
554    interface Callbacks {
555        void updateBrightness();
556    }
557
558    private static final class AmbientLightRingBuffer{
559        // Proportional extra capacity of the buffer beyond the expected number of light samples
560        // in the horizon
561        private static final float BUFFER_SLACK = 1.5f;
562        private static final int DEFAULT_CAPACITY =
563            (int) Math.ceil(AMBIENT_LIGHT_HORIZON * BUFFER_SLACK / LIGHT_SENSOR_RATE_MILLIS);
564        private float[] mRingLux;
565        private long[] mRingTime;
566        private int mCapacity;
567
568        // The first valid element and the next open slot.
569        // Note that if mCount is zero then there are no valid elements.
570        private int mStart;
571        private int mEnd;
572        private int mCount;
573
574        public AmbientLightRingBuffer() {
575            this(DEFAULT_CAPACITY);
576        }
577
578        public AmbientLightRingBuffer(int initialCapacity) {
579            mCapacity = initialCapacity;
580            mRingLux = new float[mCapacity];
581            mRingTime = new long[mCapacity];
582        }
583
584        public float getLux(int index) {
585            return mRingLux[offsetOf(index)];
586        }
587
588        public long getTime(int index) {
589            return mRingTime[offsetOf(index)];
590        }
591
592        public void push(long time, float lux) {
593            int next = mEnd;
594            if (mCount == mCapacity) {
595                int newSize = mCapacity * 2;
596
597                float[] newRingLux = new float[newSize];
598                long[] newRingTime = new long[newSize];
599                int length = mCapacity - mStart;
600                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
601                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
602                if (mStart != 0) {
603                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
604                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
605                }
606                mRingLux = newRingLux;
607                mRingTime = newRingTime;
608
609                next = mCapacity;
610                mCapacity = newSize;
611                mStart = 0;
612            }
613            mRingTime[next] = time;
614            mRingLux[next] = lux;
615            mEnd = next + 1;
616            if (mEnd == mCapacity) {
617                mEnd = 0;
618            }
619            mCount++;
620        }
621
622        public void prune(long horizon) {
623            if (mCount == 0) {
624                return;
625            }
626
627            while (mCount > 1) {
628                int next = mStart + 1;
629                if (next >= mCapacity) {
630                    next -= mCapacity;
631                }
632                if (mRingTime[next] > horizon) {
633                    // Some light sensors only produce data upon a change in the ambient light
634                    // levels, so we need to consider the previous measurement as the ambient light
635                    // level for all points in time up until we receive a new measurement. Thus, we
636                    // always want to keep the youngest element that would be removed from the
637                    // buffer and just set its measurement time to the horizon time since at that
638                    // point it is the ambient light level, and to remove it would be to drop a
639                    // valid data point within our horizon.
640                    break;
641                }
642                mStart = next;
643                mCount -= 1;
644            }
645
646            if (mRingTime[mStart] < horizon) {
647                mRingTime[mStart] = horizon;
648            }
649        }
650
651        public int size() {
652            return mCount;
653        }
654
655        public boolean isEmpty() {
656            return mCount == 0;
657        }
658
659        public void clear() {
660            mStart = 0;
661            mEnd = 0;
662            mCount = 0;
663        }
664
665        @Override
666        public String toString() {
667            final int length = mCapacity - mStart;
668            float[] lux = new float[mCount];
669            long[] time = new long[mCount];
670
671            if (mCount <= length) {
672                System.arraycopy(mRingLux, mStart, lux, 0, mCount);
673                System.arraycopy(mRingTime, mStart, time, 0, mCount);
674            } else {
675                System.arraycopy(mRingLux, mStart, lux, 0, length);
676                System.arraycopy(mRingLux, 0, lux, length, mCount - length);
677
678                System.arraycopy(mRingTime, mStart, time, 0, length);
679                System.arraycopy(mRingTime, 0, time, length, mCount - length);
680            }
681            return "AmbientLightRingBuffer{mCapacity=" + mCapacity
682                + ", mStart=" + mStart
683                + ", mEnd=" + mEnd
684                + ", mCount=" + mCount
685                + ", mRingLux=" + Arrays.toString(lux)
686                + ", mRingTime=" + Arrays.toString(time)
687                + "}";
688        }
689
690        private int offsetOf(int index) {
691            if (index >= mCount || index < 0) {
692                throw new ArrayIndexOutOfBoundsException(index);
693            }
694            index += mStart;
695            if (index >= mCapacity) {
696                index -= mCapacity;
697            }
698            return index;
699        }
700    }
701}
702