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